Skip to main content

FastAPI vs Django 5: An Honest Choice After 8 Months of Pain

Константин Потапов
20 min

Honest comparison of FastAPI and Django 5 after 8 months of migrations both ways. Real stories, Django async pitfalls, cases of saving $50k/year and returning to Django in a week. No marketing—only practice.

FastAPI vs Django 5: An Honest Choice After 8 Months of Pain

"We rewrote the entire API in FastAPI in a month—and production became 2x faster!"

"We tried FastAPI—and returned to Django in a week. Here's why."

Two real cases from my 2025 practice. Both projects made conscious choices, both got what they wanted. But the results are opposite.

Spoiler: There's no "best" framework. There's the right choice for your task. And now I'll show you how not to screw up this choice.

In 2025, the Django vs FastAPI discussion stopped being a religious war. Django got async (but with so many pitfalls, holy shit), and FastAPI matured into a production-grade tool (and stopped being a "hipster toy").

I spent the last 8 months migrating projects between frameworks in both directions. Lost a couple hundred hours on mistakes, but now I know where each really shines and where it's lying in marketing.

Boxers Enter the Ring

Django 5.2 LTS: The Old Wolf Who Learned Async Tricks

20 years on the market. While FastAPI was still crawling in diapers, Django was already building banks and government portals. Current version 5.2 LTS (April 2025) is like a Mercedes S-Class: expensive to maintain, but reliable as a Swiss watch.

What it can do:

  • ✅ Async views — works, but there's a nuance (read: pitfalls)
  • ✅ Async ORM (aget(), acreate(), asave()) — half the methods
  • ✅ Legendary admin panel — CRUD for 50 tables in 5 minutes
  • ✅ 4800+ packages — there's a solution for EVERYTHING
  • Async transactions — NO (it hurts, really hurts)
  • Async admin — NO (and not planned, accept it)

Metaphor: Django is a Swiss Army knife with 100 tools. You use 15 of them, but damn, it's so convenient that the other 85 exist.

When Django pisses you off:

  • Enabled async views → half the middleware became sync → got performance penalty
  • Wrote a transaction → wrapped in sync_to_async → crying into pillow
  • Wanted to customize admin → after 3 hours realized it's easier to write from scratch

When Django saves your life:

  • MVP needed yesterday → admin in 10 minutes → client happy
  • Junior screwed up migration → ./manage.py migrate fixed everything → junior alive
  • Production crashed at 3 AM → Django ORM logs showed everything → fixed in 15 minutes

FastAPI 0.124: Young and Cocky, Knows Its Worth

7 years on the market. During this time, FastAPI went from "hipster toy" to a serious tool for high-load APIs. Current version 0.124 (December 2025) is like a Tesla: fast, techy, but sometimes you don't understand why the heater doesn't work.

What it can do:

  • ✅ Async everything — out of the box, no crutches
  • ✅ OpenAPI/Swagger — auto-generation, product manager in ecstasy
  • ✅ Pydantic V2 — validation faster than you blink
  • ✅ Dependency Injection — like enterprise Java, but not terrible
  • ✅ WebSocket/SSE — native, no dancing with tambourine
  • No admin panel — you'll have to build or pay for third-party
  • No migrations — Alembic manually, welcome to pain

Metaphor: FastAPI is LEGO for adults. You assemble only what you need. But you'll have to read the instructions.

When FastAPI pisses you off:

  • Need admin panel → 2 days on FastAPI-Admin → it's buggy → another day on crutches
  • Forgot Alembic migration → production crashed → restoring schema from logs
  • Junior asks "How to do this?" → show 5 ways → he's shocked

When FastAPI saves your life:

  • API is slow → rewrote 3 endpoints in FastAPI → load 2x less
  • Need docs for partners → /docs → no questions
  • WebSocket for chat → 50 lines of code → works out of the box

Round 1: Performance (or "Why CEO Looks at Grafana Charts")

Real Story: "Our API Crashed on Black Friday"

Friday, 11:45 PM. Traffic increased 10x. Django API started giving 503. I'm sitting with a laptop in a 24-hour coffee shop (because home internet is down, of course) frantically scaling instances.

Result: 8 Django instances handled it. Rewrote to FastAPI — 3 instances were enough with margin.

Savings: $1200/month on infrastructure. CEO happy. I got a bonus. FastAPI got a ⭐️ on GitHub.

Numbers That Matter

Benchmark on same hardware (4 vCPU, 8GB RAM, uvicorn/gunicorn):

# FastAPI (async endpoint)
from fastapi import FastAPI
 
app = FastAPI()
 
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id, "name": "Item"}
 
# Result: ~25000 req/s (wrk benchmark)
# Django 5 (async view)
from django.http import JsonResponse
 
async def read_item(request, item_id):
    return JsonResponse({"item_id": item_id, "name": "Item"})
 
# Result: ~18000 req/s (wrk benchmark)

Verdict: FastAPI is 30-40% faster. In money, that's 2-3 servers per every 10.

BUT (and this is a big BUT): Django async shows these 18k req/s only if:

  1. ✅ You have NO sync middleware (say goodbye to sessions)
  2. ✅ You have NO admin panel (say goodbye to convenience)
  3. ✅ You're ready to wrap transactions in sync_to_async (say goodbye to beautiful code)

Reality: In production with standard Django setup you'll get ~12k req/s. Difference with FastAPI is already 2x.

Django Async Pitfalls That Cost Me a Weekend

# I wrote async view. Beautiful, right?
async def my_view(request):
    users = [u async for u in User.objects.all()]
    return JsonResponse({"users": users})
 
# Launched in production. And...
# Performance: 12k req/s instead of promised 18k

Problem: I had SessionMiddleware (sync) in MIDDLEWARE. Django silently switched to sync mode on every request.

Solution: Removed all sync middleware. Now I have no sessions. And admin doesn't work. Thanks, Django.

Database Work (async ORM)

# FastAPI + SQLAlchemy 2.0 async
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
 
async def get_users(db: AsyncSession):
    result = await db.execute(select(User).limit(100))
    return result.scalars().all()
 
# 100 records: ~15ms
# Django 5 async ORM
from django.contrib.auth.models import User
 
async def get_users():
    users = [user async for user in User.objects.all()[:100]]
    return users
 
# 100 records: ~18ms

Verdict: For simple SELECT, the difference is 3ms. Negligible. Choose what's more convenient.

BUT (again this BUT): Transactions in Django async DON'T WORK.

# ❌ This WON'T work
async with transaction.atomic():
    await User.objects.acreate(...)
    await Profile.objects.acreate(...)
# RuntimeError: atomic() doesn't support async
 
# ✅ Have to do this
@sync_to_async
def create_user_with_profile(data):
    with transaction.atomic():
        user = User.objects.create(...)
        Profile.objects.create(user=user, ...)
# We're in sync mode again. Where's async?

In SQLAlchemy: async transactions work out of the box. No crutches.


Round 2: Type Hints (or "How I Learned to Love Type Hints and Stop Worrying About Production")

Story: "How Pydantic Saved Me From Firing"

Monday, 10:00 AM. Product manager sends new API requirements. Need to add 15 fields, change validation in 20 places.

With Django: 2 hours editing serializers.py, writing clean_* methods, updating docs manually. Forgot one field → bug in production → angry clients.

With FastAPI: Changed Pydantic model. OpenAPI updated automatically. Frontend saw changes in /docs. Nobody got hurt.

FastAPI (Pydantic V2)

from pydantic import BaseModel, Field, validator
from typing import Annotated
 
class UserCreate(BaseModel):
    email: str = Field(..., pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
    age: Annotated[int, Field(ge=18, le=120)]
 
    @validator("email")
    def email_must_be_lowercase(cls, v):
        return v.lower()
 
@app.post("/users/")
async def create_user(user: UserCreate):
    # Automatic validation, type conversion, OpenAPI generation
    return user

Pros:

  • Auto-generates OpenAPI schema
  • Type-level validation
  • Type conversion from query/path parameters
  • Built-in documentation (Swagger UI)

Django 5 (Forms + Serializers)

from django import forms
from rest_framework import serializers
 
class UserCreateForm(forms.Form):
    email = forms.EmailField()
    age = forms.IntegerField(min_value=18, max_value=120)
 
    def clean_email(self):
        return self.cleaned_data['email'].lower()
 
# Or with DRF
class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    age = serializers.IntegerField(min_value=18, max_value=120)

Pros:

  • Time-tested validation system
  • Django ORM integration
  • Admin automatically uses forms
  • DRF serializers for API

Verdict: FastAPI wins by knockout.

Why: Every change in Pydantic model automatically:

  • ✅ Updates OpenAPI schema
  • ✅ Validates on input
  • ✅ Generates examples in /docs
  • ✅ Shows errors with precise pointers

In Django DRF: all the same, but manually. And with crutches. And don't forget to update drf-spectacular schema. And pray nothing breaks.

Time to change API contract:

  • Django DRF: 30-60 minutes
  • FastAPI: 5-10 minutes

Difference: 2 hours/day × 20 working days = 40 hours/month. That's a week of work.

3. Ecosystem and Built-in Tools

FeatureDjango 5FastAPI
Admin panelBuilt-in, powerfulNo (third-party: FastAPI-Admin, SQLAdmin)
ORMDjango ORM (async)SQLAlchemy, Tortoise ORM, SQLModel
Migrationsdjango-admin migrateAlembic (manual)
Authenticationdjango.contrib.authOAuth2/JWT (manual or libraries)
CORS/CSRFMiddleware + decoratorsCORSMiddleware (Starlette)
Background tasksCelery, Django-QARQ, Celery, BackgroundTasks
WebSocketsDjango ChannelsBuilt-in support
GraphQLGraphene-DjangoStrawberry, Ariadne
TestingDjango TestCasepytest + httpx

Verdict: Django is full "batteries included," FastAPI is a flexible constructor.

4. API Documentation

FastAPI:

@app.post("/items/",
          summary="Create item",
          response_description="Created item details",
          tags=["items"])
async def create_item(
    item: ItemCreate,
    x_token: Annotated[str, Header(description="API token")]
):
    """
    Create item with metadata:
 
    - **name**: item name (required)
    - **price**: item price in USD
    """
    return item

Automatically generates:

  • Swagger UI at /docs
  • ReDoc at /redoc
  • OpenAPI JSON at /openapi.json

Django + DRF:

from rest_framework.decorators import api_view
from drf_spectacular.utils import extend_schema
 
@extend_schema(
    summary="Create item",
    tags=["items"],
    request=ItemSerializer,
    responses={201: ItemSerializer}
)
@api_view(['POST'])
def create_item(request):
    serializer = ItemSerializer(data=request.data)
    if serializer.is_valid():
        return Response(serializer.data, status=201)
    return Response(serializer.errors, status=400)

Requires:

  • Installing drf-spectacular
  • Manual configuration
  • Decorators for each endpoint

Verdict: FastAPI automates documentation "out of the box."

5. Learning Curve

Django:

  • More concepts (MTV pattern, apps, signals, middleware layers)
  • 5000+ page documentation
  • Need to learn ORM, forms, templates, admin
  • But unified style across entire ecosystem

FastAPI:

  • Less magic, more transparent behavior
  • Based on Python standards (type hints, async/await)
  • More compact documentation
  • But requires knowing Pydantic, SQLAlchemy, Alembic separately

Verdict: FastAPI easier to start API-only projects, Django easier for full-stack monoliths.


Real Stories: The Choice That Changed Everything

Case 1: "How Django Saved a Startup in 3 Days"

Situation: EdTech startup. Investor gave 72 hours for MVP demo. Need admin for content moderation, API for mobile, and all this yesterday.

What we did:

  • Django admin → 4 models in 30 minutes → moderators working
  • DRF → 10 endpoints in 2 hours → mobile gets data
  • Heroku → git push → in production

Result: Demo ready in 48 hours. Investor put in $500k. Startup alive.

Why Django: Admin out of the box. This was critical. FastAPI would require 2 more days to develop admin.


Case 2: "How FastAPI Saved $50k/Year on Servers"

Situation: Fintech API handles 100k requests/min. Django on 40 c5.2xlarge instances (AWS). Bill: $6k/month.

What we did:

  • Rewrote 15 critical endpoints in FastAPI
  • Async SQLAlchemy instead of Django ORM
  • Added connection pooling

Result:

  • 40 instances → 16 instances
  • $6k/month → $2.4k/month
  • Latency P99: 250ms → 95ms

Why FastAPI: Async everything. Native. No crutches with sync_to_async. And most importantly—no sync middleware penalty.


Case 3: "How We Returned From FastAPI to Django in a Week"

Situation: SaaS company. CTO decided to "be trendy" and rewrote everything in FastAPI. After a month:

Problems:

  • Admin on FastAPI-Admin — buggy, slow
  • Alembic migrations — junior constantly screws up
  • No centralized authentication
  • Each microservice configured differently

Solution: Rolled back to Django. Backend on DRF, admin out of the box, everything works.

Why returned: Team of 3 people can't handle microservice architecture. Django monolith easier to maintain.

Lesson: Don't choose technology by hype. Choose by team size.


Checklist: Django or FastAPI? (60 Seconds to Decision)

Choose Django if you answered "YES" to 3+ questions:

Example architecture:

Django 5 (async views)
  ↓
PostgreSQL (async psycopg3)
  ↓
Celery (background tasks)
  ↓
Redis (cache + broker)
  • Need built-in admin panel?
  • Project includes server-side rendering?
  • Team already knows Django?
  • Need authentication out of the box?
  • Planning monolithic architecture?
  • MVP speed is important?
  • Have legacy Django code to integrate?

Choose FastAPI if you answered "YES" to 3+ questions:

Example architecture:

FastAPI (async handlers)
  ↓
SQLAlchemy 2.0 (async engine)
  ↓
Redis (async aioredis)
  ↓
RabbitMQ (aio-pika for events)
  • Project is API-only (no HTML templates)?
  • Maximum performance is important?
  • Need OpenAPI auto-generation?
  • Planning WebSocket/SSE?
  • Team prefers type hints?
  • Microservice architecture?
  • High load (I/O-bound operations)?

Hybrid Approach

Can use both!

Frontend (Next.js)
    ↓
FastAPI (public API, high load)
    ↓
Django (admin, internal tools)
    ↓
Shared PostgreSQL

Real case:

  • FastAPI for mobile API (100k+ req/min)
  • Django admin for content moderation
  • Shared DB, but different services

Migration Between Frameworks

Django → FastAPI

Strategy:

  1. Create FastAPI service in parallel
  2. Move endpoints gradually
  3. Use Django ORM via sync_to_async
  4. Switch routing in nginx/traefik
# Temporary Django ORM adapter in FastAPI
from asgiref.sync import sync_to_async
from django.contrib.auth.models import User
 
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await sync_to_async(User.objects.get)(id=user_id)
    return {"id": user.id, "email": user.email}

FastAPI → Django

Rare case, but possible:

  • Use Pydantic models in DRF
  • Import business logic as is
  • Wrap async code in sync views

Production Performance: Real Numbers

Case 1: JSON API (10k req/s)

Configuration: 4 vCPU, 8GB RAM, Postgres, Redis

MetricDjango 5 + GunicornFastAPI + Uvicorn
P50 latency45ms32ms
P99 latency180ms95ms
Max RPS850012000
Memory (idle)250MB180MB

Case 2: CRUD with DB (1k req/s)

Operation: SELECT + INSERT in transaction

MetricDjango 5 asyncFastAPI + SQLAlchemy
Avg latency85ms78ms
DB pool usage60%55%
Error rate0.01%0.01%

Conclusion: Difference negligible at moderate load, FastAPI wins at high-load.


The Truth About Django Async (That Documentation Doesn't Tell)

This is a sore subject. Django marketing screams "We have async!" But in practice it's "We have async, but..."

Spoiler: I lost a weekend figuring out why my "async" Django is slower than sync version.

Pitfall #1: Admin Panel — Forever Sync

Django admin DOESN'T support async. At all. In no way. And not planned.

Why this is a problem:

# ❌ Tried to make async admin
class MyAdmin(admin.ModelAdmin):
    async def get_queryset(self, request):
        return await MyModel.objects.all()
 
# Launched → 500 Internal Server Error
# Went to logs → SynchronousOnlyOperation
# Went to tickets → Won't fix

Real case: Client required admin. I wrote async API. Django forcibly kept sync middleware. Got worst of both worlds: async complexity + sync performance.

Solution: Use FastAPI for API + Django separately only for admin. Two services. Two deploys. Double pain.


Pitfall #2: Transactions — For Sync Dinosaurs

# ❌ This WON'T work
from django.db import transaction
 
async def create_user(data):
    async with transaction.atomic():  # Error!
        user = await User.objects.acreate(**data)
        await Profile.objects.acreate(user=user)

Workaround:

# ✅ Use sync_to_async
from asgiref.sync import sync_to_async
 
@sync_to_async
def create_user_with_transaction(data):
    with transaction.atomic():
        user = User.objects.create(**data)
        Profile.objects.create(user=user)
 
# In async view
await create_user_with_transaction(data)

Pitfall #3: Performance Penalty When Mixing Sync/Async

If your project has at least one sync middleware, Django loses async advantages:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',  # sync
    'django.contrib.sessions.middleware.SessionMiddleware',  # sync!
    # ... other sync middleware
]

Consequences:

  • Switching sync → async → sync costs ~1ms per transition
  • Django uses thread per request instead of async I/O
  • Async views work, but without performance gain

Solution: Remove ALL sync middleware or use FastAPI.

Pitfall #4: ORM Marked as "Async-Unsafe"

Django ORM has global state that's not coroutine-aware:

# ⚠️ May cause race conditions
async def concurrent_updates():
    tasks = [
        User.objects.filter(id=1).aupdate(score=F('score') + 1)
        for _ in range(100)
    ]
    await asyncio.gather(*tasks)
# Result may be unpredictable

What Actually Works in Async Mode?

✅ Works:

  • Async views (async def view(request))
  • Async ORM queries: aget(), acreate(), asave(), adelete(), aupdate()
  • Async iteration: async for user in User.objects.all()
  • Async middleware (if ALL middleware async)
  • WebSocket via Django Channels (separate ASGI server)

❌ DOESN'T work:

  • Admin panel
  • Transactions (atomic())
  • Many third-party packages (need to check compatibility)
  • Template rendering in sync middleware

When Does Django Async Make Sense?

✅ Use if:

  • Need to make many external HTTP requests in one view
  • Can remove all sync middleware
  • Don't use transactions (or ready for sync_to_async)
  • Admin not critical for project

❌ DON'T use if:

  • Actively use admin
  • Lots of transactional logic
  • Have sync middleware (sessions, CSRF, etc.)
  • Team not ready for limitations

Example where Django async works well:

import httpx
 
async def aggregate_data(request):
    async with httpx.AsyncClient() as client:
        # Parallel requests to external APIs
        responses = await asyncio.gather(
            client.get("https://api1.example.com/data"),
            client.get("https://api2.example.com/data"),
            client.get("https://api3.example.com/data"),
        )
    # Simple aggregation without transactions
    return JsonResponse({"data": [r.json() for r in responses]})

Example where FastAPI is better:

# Complex business logic with transactions
async def create_order(data):
    # In Django would have to wrap in sync_to_async
    # In FastAPI works natively with async SQLAlchemy
    async with async_session() as session:
        async with session.begin():  # Transactions work!
            order = Order(**data)
            session.add(order)
            await session.flush()
 
            for item in data['items']:
                order_item = OrderItem(order_id=order.id, **item)
                session.add(order_item)
 
            await session.commit()
    return order

What's New in 2025?

Django 5.2 LTS (Released April 2, 2025)

Django 5.2 became LTS release with support until April 2028. Key features:

  • Composite Primary Keys — composite primary keys from multiple fields
  • Automatic model imports in python manage.py shell
  • MySQL utf8mb4 by default (full emoji support)
  • Async methods for User model and permissions
  • Python 3.10-3.14 support
  • BUT: async admin still NOT implemented

FastAPI 0.124+ (Current version: 0.124.4, December 2025)

  • Pydantic v1 + v2 — simultaneous support of both versions for smooth migration
  • Improved security — correct HTTP 401 instead of 403 when credentials missing
  • Improved CLIfastapi run --entrypoint module:app
  • Fixed hierarchical propagation security scopes
  • Dependency caching without scopes for performance

So What to Choose? (Final Verdict)

After 8 months of migrations, sleepless nights and thousands of lines of code read, here's my honest recommendation:

Django 5 — When Admin Is More Important Than Speed

Choose if:

  • You need admin right now
  • Team of 1-3 people
  • MVP needed yesterday
  • Client doesn't understand what API is

Reality: Yes, Django is slow. Yes, async is half-baked. But damn it, it works. And works out of the box.

Price of choice: $500-1000/month overpay on servers. But time savings are priceless.


FastAPI — When Speed Is Critical

Choose if:

  • Traffic > 10k req/min
  • Server budget limited
  • Team understands async
  • Admin not needed (or ready to spend a week on crutches)

Reality: FastAPI is fast. Very fast. But you'll have to mess with Alembic, authentication, and build admin yourself.

Price of choice: +1-2 weeks on setup. But then—profit in performance.


Hybrid Approach — For Those Who Understood Life

Best solution in 2025:

FastAPI (public API) + Django (admin) = ❤️

Why this works:

  • FastAPI handles load (save on servers)
  • Django gives admin (save development time)
  • Shared PostgreSQL (one source of truth)

Real case: I implemented this on 3 projects. Everyone happy. CEO happy (savings), developers happy (beautiful code), content managers happy (admin works).


Last Advice

Don't choose framework by hype.

Django old? Yes. But it outlived 100500 JS frameworks and lives on.

FastAPI trendy? Yes. But in 3 years something new will appear.

Choose by task:

  • Admin critical? → Django
  • Performance critical? → FastAPI
  • Need both? → Both

P.S. If after this article you still don't know what to choose—write me. We'll analyze your case in 15 minutes. Free. Because I'm tired of seeing people suffer from wrong choice.


Useful Links:


Share Your Experience!

I shared my 8 months of pain. Now it's your turn:

  • What framework do you use?
  • What pitfalls did you encounter?
  • Did you regret your choice?

Write in comments or Telegram. Let's discuss, compare, laugh at mistakes.

Need consultation on stack choice? Write to email — I'll analyze your case and give honest recommendation. No sales, no fluff, only practice.

Liked the article? Share with a colleague who still chooses framework by GitHub stars count. Save their nerves (and weekends).


Subscribe to updates on Telegram — I write about Python, architecture and development pain. No fluff, only practice.