I have been building Python web applications for four years. Django was my default for the first two. FastAPI replaced it for most of my work in the last two. But I had never done a proper side by side comparison of all the major options at once.
So I built the same API in Django, FastAPI, Flask, and Starlette. Same endpoints, same database, same business logic. I tracked performance under load, development speed, code complexity, and how each one handled production requirements.
One result I did not expect at all. Here is the honest breakdown.
The Test
The API had five endpoints. A user registration endpoint, a login endpoint with JWT generation, a paginated list endpoint hitting a PostgreSQL database, a detail endpoint with related data, and a background task endpoint that triggered an async job.
This covers the patterns that appear in almost every real application. Authentication, database access, pagination, related data, and async operations.
I ran each framework on identical hardware with identical database configuration. Load testing used 100 concurrent users over 5 minutes.
Django
Django is the most complete Python web framework available. It ships with an ORM, an admin panel, authentication, form handling, and a migration system out of the box.
Development speed: Django was the fastest to get running. The project structure is opinionated and the scaffolding tools handle most of the boilerplate. The user registration and login endpoints were working in under 30 minutes using Django’s built-in authentication.
Performance under load:
Average response time: 187ms
P95 response time: 312ms
Requests per second: 284
Error rate: 0.2%
Solid but not exceptional. Django’s synchronous architecture means each worker handles one request at a time. Under concurrent load, requests queue behind each other.
Code complexity: The paginated list endpoint with related data was the cleanest of all four frameworks. Django’s ORM handles related data elegantly.
# Django - clean and readable
def order_list(request):
orders = Order.objects.select_related("user").prefetch_related("items")
paginator = Paginator(orders, 20)
page = paginator.get_page(request.GET.get("page", 1))
return JsonResponse({"orders": list(page.object_list.values())})
Where it struggled: The background task endpoint required Celery for proper async job handling. That is an additional dependency, additional infrastructure, and additional configuration that the other frameworks handle more natively.
Verdict: Best choice for applications that need admin panels, content management, or rapid prototyping with batteries included.
Flask
Flask is the minimalist option. It gives you routing and request handling and nothing else. Every other feature is a decision you make and a library you add.
Development speed: Flask was the slowest to get running because every feature required a separate decision. SQLAlchemy for the ORM, Flask-JWT-Extended for authentication, Flask-Migrate for migrations. Each one required configuration and each one had its own conventions.
Performance under load:
Average response time: 201ms
P95 response time: 389ms
Requests per second: 261
Error rate: 0.4%
Slightly slower than Django under load. Flask is also synchronous and the overhead of its extension ecosystem adds up under concurrent requests.
Code complexity: The flexibility that makes Flask appealing also makes it inconsistent. With four developers on a Flask project, you often end up with four different patterns for the same operation.
# Flask - more explicit but more verbose
@app.route("/orders")
def order_list():
page = request.args.get("page", 1, type=int)
orders = Order.query.options(
joinedload(Order.user),
subqueryload(Order.items)
).paginate(page=page, per_page=20)
return jsonify({"orders": [o.to_dict() for o in orders.items]})
Where it struggled: Background tasks required the same Celery setup as Django. There is no native async support in Flask’s core.
Verdict: Good for small APIs and projects where you want full control over every dependency. Becomes harder to maintain at scale due to inconsistency across the codebase.
FastAPI
FastAPI is built on Starlette and uses Python type hints for automatic validation and documentation generation. It is the framework I use for most of my production work.
Development speed: FastAPI was competitive with Django for getting running. Pydantic models handle validation automatically and the auto-generated documentation at /docs eliminates a separate documentation step.
Performance under load:
Average response time: 94ms
P95 response time: 187ms
Requests per second: 891
Requests per second: 0.1%
FastAPI was dramatically faster than Django and Flask under concurrent load. The async architecture means workers handle multiple requests simultaneously while waiting for database responses.
Code complexity: The type hint driven approach keeps code clean and self-documenting.
# FastAPI - type hints do the heavy lifting
@app.get("/orders", response_model=PaginatedOrders)
async def order_list(page: int = 1, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Order).options(selectinload(Order.items))
.offset((page - 1) * 20).limit(20)
)
return {"orders": result.scalars().all()}
Where it struggled: No built-in admin panel. Authentication requires manual implementation or a library. The async model requires understanding async Python properly or performance suffers.
Verdict: Best choice for high traffic APIs, microservices, and any application where response time under concurrent load matters.
Starlette
This is the one that surprised me.
Starlette is the ASGI framework that FastAPI is built on. Most Python developers know it exists but few use it directly. FastAPI adds type hint validation, automatic documentation, and dependency injection on top of Starlette. Using Starlette directly means writing all of that yourself.
I included it expecting it to be impractical for real applications. I was wrong.
Development speed: Slower than FastAPI because there is no automatic validation or documentation. Every endpoint requires explicit request parsing and response construction.
Performance under load:
Average response time: 71ms
P95 response time: 134ms
Requests per second: 1,247
Error rate: 0.05%
Starlette was 40% faster than FastAPI and over 4x faster than Django. With no validation layer and no dependency injection overhead, requests move through the stack with minimal processing.
# Starlette - explicit but extremely fast
async def order_list(request):
page = int(request.query_params.get("page", 1))
async with AsyncSession(engine) as db:
result = await db.execute(
select(Order).offset((page - 1) * 20).limit(20)
)
orders = result.scalars().all()
return JSONResponse({"orders": [o.__dict__ for o in orders]})
Where it struggled: No validation means validation errors become runtime errors instead of automatic 422 responses. No dependency injection means shared logic requires explicit passing or global state. Building a production application in Starlette requires rebuilding pieces that FastAPI provides for free.
Verdict: The right choice for extremely high throughput applications where every millisecond matters and you are willing to build the validation and documentation layer yourself. Not practical for most teams.
The Head to Head Results
Django Average response: 187 ms Requests per second: 284 Development speed: Fastest Best for: Full applications and admin panels
Flask Average response: 201 ms Requests per second: 261 Development speed: Slowest Best for: Small APIs and full control
FastAPI Average response: 94 ms Requests per second: 891 Development speed: Fast Best for: High traffic APIs and microservices
Starlette Average response: 71 ms Requests per second: 1247 Development speed: Slow Best for: Maximum throughput and performance
What Actually Surprised Me
I assumed Starlette would be impractical, but the benchmarks changed my view.
For certain use cases, especially high throughput microservices with experienced teams ready to handle extra boilerplate for better performance, Starlette is a strong option. A 40% speed gain over FastAPI is significant at scale.
What surprised me was the cost in development time. A user registration endpoint that took 25 minutes in FastAPI required 90 minutes in Starlette because validation and error handling had to be built from scratch.
This trade off only makes sense when the performance gain leads to meaningful infrastructure savings.
What surprised me was the cost in development time. A user registration endpoint that took 25 minutes in FastAPI required 90 minutes in Starlette because validation and error handling had to be built from scratch.
This trade off only makes sense when the performance gain leads to meaningful infrastructure savings.
Which One Should You Use
For most applications, FastAPI is the right choice today. The performance advantage over Django and Flask is significant and the developer experience is better than Starlette.
Use Django if you need an admin panel, built-in authentication, or a complete batteries-included framework for a content heavy application.
Use Flask if your team has deep Flask expertise and the application is small enough that the inconsistency problem does not matter yet.
Use Starlette if you are building a high throughput microservice and your team is experienced enough to build the validation layer without the structure FastAPI provides.
Comments
Loading comments…