DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Beyond Django and Flask: How FastAPI Became Python's Fastest-Growing Framework for Production APIs
  • Django Architecture vs FastAPI: A Learning Path
  • Mistakes That Django Developers Make and How To Avoid Them
  • How To Build a Simple GitHub Action To Deploy a Django Application to the Cloud

Trending

  • Jeffrey Microscope for Generating Flame Graphs in Java
  • Architecting Trustworthy AI: Engineering Patterns for High-Stakes Environments
  • AI-Augmented React Development: How I Rebuilt My Workflow Without Losing Control of the Code
  • The Future of Agentic AI
  1. DZone
  2. Coding
  3. Frameworks
  4. FastAPI + Django in Production: Lessons From a Hybrid Stack

FastAPI + Django in Production: Lessons From a Hybrid Stack

Learn how to combine Django and FastAPI, including async ORM pitfalls, thread and database connection issues, testing challenges, and practical solutions.

By 
Evgeniia Chibisova user avatar
Evgeniia Chibisova
·
Jul. 31, 26 · Analysis
Likes (1)
Comment
Save
Tweet
Share
97 Views

Join the DZone community and get the full member experience.

Join For Free

Picture the scene: One of the services in your backend is a mature Django app that no one has the resources, time, or, frankly, the will to rewrite. The ORM, the admin panel, and the broader ecosystem all earn their keep.

But you’re looking for the best way to describe your API, and FastAPI catches your eye. It looks like a great fit: native typing, pydantic-based validation, OpenAPI out of the box, and of course the support for async endpoints.

That's the situation our team found itself in - we decided to use both frameworks and take from each what suited us best. Not everything went smoothly — this post is what we built, what broke afterward, and what we learned.

The First Win

So we wired it up, and it works. FastAPI runs as the ASGI application, and the existing Django app plugs into it. 

Python
 
# asgi.py
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

from django.core.asgi import get_asgi_application
from fastapi import FastAPI

app = FastAPI()

django_app = get_asgi_application()
app.mount("/legacy", django_app)


Great! Now:

  • Both Django and FastAPI endpoints live side by side, with no pressure to refactor everything in a single day — that was important for us.
  • In the new parts of the app, Django steps back into a single role: communicating with the database through its models.
  • Endpoints can be either sync or async.

That was the win. But there was the other side also.

Pitfall 1: Async Endpoints Started Running One at a Time

When you reach out to external services, chances are you also want to enrich the request with something from your database, or save the result back to it (we did). Here's a tiny example: A single async handler that fetches data about Order from the database (we use Postgres) and forwards it to an external payment provider.

Python
 
from asgiref.sync import sync_to_async
from fastapi import FastAPI

app = FastAPI()

@app.post("/orders/{order_id}/dispatch")
async def dispatch_order(order_id: int) -> OrderDTO:
    order = await sync_to_async(get_order)(order_id)    # fetch from DB
    await client.send_order(order)                      # call external service
    return order

# code that uses a Django model
def get_order(order_id: int) -> OrderDTO:
    order = Order.objects.get(id=order_id)
    return OrderDTO(id=order.id, amount=order.amount)


Inside an async function, you can’t call the Django ORM synchronously. The documented approach is sync_to_async, which moves the synchronous call to a separate thread so it doesn’t block the event loop.

Now let's see what happens under concurrent load. Drop a three-second sleep into get_order:

Python
 
from django.db import connection

def get_order(order_id: int) -> OrderDTO:
    order = Order.objects.get(id=order_id)

    with connection.cursor() as cursor:
        cursor.execute("SELECT pg_sleep(3);")

    return OrderDTO(id=order.id, amount=order.amount)


And fire three requests in parallel:

Shell
 
URL="http://localhost:8000/orders/1/dispatch"

curl -s -o /dev/null -X POST -w "%{time_total}s\n" "$URL" &
curl -s -o /dev/null -X POST -w "%{time_total}s\n" "$URL" &
curl -s -o /dev/null -X POST -w "%{time_total}s\n" "$URL"

>> 3.012s
>> 6.024s
>> 9.037s


We expected ~3 seconds and got nine. The handlers ran one after another, not concurrently. And if you log the thread and database connection IDs from inside get_order, all three requests print the same values.

Why? By default sync_to_async(get_order) runs with thread_sensitive=True, which means the function runs in the same thread as all other thread_sensitive functions. A standalone Django ASGI app does extra work here: it opens a fresh context per request, so requests run in parallel. The benchmark suggests that in our setup FastAPI doesn't: all three sync_to_async calls land on the same thread and line up one behind another.

The event loop itself stays free, by the way: a purely async route keeps responding while the three /dispatch requests wait in that queue. But three async handlers with ORM calls queue up on the same thread, sharing the same connection. 

For a moment we hoped Order.objects.aget(...) or other Django async ORM helpers would save us here. They won't: for now under the hood they call the same sync_to_async.

Can we just flip to sync_to_async(..., thread_sensitive=False)? Probably not - it is not a safe default. Django carries a lot of per-request state in thread-locals: the current DB connection, transaction.atomic(), etc. The Django docs say:  "a lot of existing Django code assumes it all runs in the same thread."

What to Do About It

No silver bullet, but two approaches hold up:

  • Split handlers by what they touch. Reserve async def for endpoints that genuinely don't touch the ORM — async-native HTTP calls, cache reads, etc. For ORM-bound endpoints, declare them as plain sync routes. FastAPI runs sync routes on its thread pool, so they actually run in parallel, and each thread gets its own Django connection. As long as these endpoints don't make many slow external calls, this can work.
  • Move the work out of the handler entirely. If your project already runs with a message broker, the possible answer to "external API + DB write inside a handler" is to stop doing it inside a handler at all. Drop an event on the bus, let consumers handle the side effects, return immediately. The catch: this only makes sense when an event-driven flow already fits your system — because it is, of course, no small refactor.

Pitfall 2: Tests That Can't See Their Own Data

Now let's write a test for get_order — a sync endpoint that reads an order from the DB. The test runs with pytest-django: we create an order in the database and call the handler.

Python
 
# app.py
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient


app = FastAPI()


@app.get("/orders/{order_id}")
def get_order(order_id: int) -> OrderDTO:
    try:
        order = Order.objects.get(id=order_id)
    except Order.DoesNotExist:
        raise HTTPException(status_code=404)
    return OrderDTO(id=order.id, amount=order.amount)


@pytest.mark.django_db
def test_get_order():
    Order.objects.create(id=1)
    response = TestClient(app).get("/orders/1")
    assert response.status_code == 200   # and we'll have 404


You get 404 Not Found. The handler ran, looked at the database, and the order was nowhere to be found.

Four facts conspire here:

  • Pytest runs your test's data setup in one thread; when the FastAPI test client calls the endpoint, the handler runs in another.
  • pytest-django wraps every test in an open transaction and rolls it back at the end. That's how the suite stays fast and isolated. The transaction lives on a single database connection.
  • Django opens a database connection per thread.
  • Postgres defaults to READ COMMITTED isolation: one connection cannot see another connection's uncommitted writes.

So: the test body runs in the pytest thread. Its Order.objects.create(...) uses connection 1, inside pytest-django's open transaction. When TestClient hits the endpoint, FastAPI dispatches the handler to a worker thread from its thread pool, on another thread with its own connection 2. 

Connection 2 looks at the database and sees no order, because connection 1 hasn't committed, so connection 1's write is effectively invisible to everyone else.

Again — What to Do?

Test in layers. Unit-test the endpoint contract with the ORM mocked - those tests don't cross thread or connection boundaries, so the visibility problem simply can't appear. Test business logic and data access in their own tests, without going through TestClient.

For cases when the full end-to-end test is still needed - the commonly suggested fix is @pytest.mark.django_db(transaction=True). This switches the test to a mode where writes actually commit, so other connections can see them. But it has its cost: pytest-django now does a database flush after every test, and the suite gets noticeably slower. On a large suite, for us "noticeably" meant minutes - too much on every run, so we use it only for exceptional cases.

The Recap

FastAPI brings obvious wins — OpenAPI docs, clean endpoint code, typing all the way through; Django gives you a greatly tested ORM and admin. Putting them in the same process gives us both — and a thread-and-connection model that doesn't behave the way we'd expect. Budget for the architecture work before you budget for the migration.

Was it worth it? Yes — we got the clean, typed API we were after, and we kept Django's ORM instead of porting the whole data layer to another framework. Would we do it again? Not sure. The trade-offs of this integration may outweigh its benefits for us, so other combinations might be a better fit.

If you’ve run into the same solution and found an approach with better trade-offs, please share; the comments are open.

Reproduce it yourself. An example with a benchmark and failing tests is in https://github.com/evchibisova/fastapi-over-django-test.

Django (web framework)

Opinions expressed by DZone contributors are their own.

Related

  • Beyond Django and Flask: How FastAPI Became Python's Fastest-Growing Framework for Production APIs
  • Django Architecture vs FastAPI: A Learning Path
  • Mistakes That Django Developers Make and How To Avoid Them
  • How To Build a Simple GitHub Action To Deploy a Django Application to the Cloud

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook