Skip to content

Web Development · Engineering

Async Django in 2026: When ASGI and Async Views Help

Async Django helps when a view waits on several slow network calls. It won't speed up CPU work or single queries, and the async ORM still runs sync underneath.

Anurag Verma

Anurag Verma

10 min read · Updated Oct 19, 2026

Async Django in 2026 -- An Honest Assessment of What Actually Works

Sponsored

Share

“Is async Django production-ready?” comes up in almost every Django team sooner or later. The short version is that the async pieces work, but they help a narrower set of views than most people expect, and a few of the limits are easy to miss until you hit them.

This post goes through what async Django supports as of Django 6.1, where it helps, where it quietly does nothing or makes things worse, and how to decide for your own views.

When does async Django actually help?

Async Django helps when a single request waits on several independent network calls, such as HTTP APIs or a remote cache, that can run at the same time. It does not speed up CPU-bound work, views with one or two database queries, or transactional code, and the async ORM still runs synchronously underneath.

Everything below is a more careful version of that paragraph.

What Django supports today

Async support arrived in steps rather than in one release. The download page lists Django 6.1 as the latest release (August 2026), with 5.2 as the current LTS. Here is what each release added, taken from the release notes:

VersionYearAsync additions
3.02019ASGI support. Internally still synchronous; async views and middleware not yet supported
3.12020Async views, async middleware, async tests and test client
4.02021Async cache API (aget, aset, aadd and friends)
4.12022Async handlers for class-based views, async ORM interface (QuerySet methods)
4.22023Model asave(), adelete(), arefresh_from_db(); async iterators in StreamingHttpResponse
5.02023Async auth functions, HttpRequest.auser(), async view decorators, Signal.asend(), aget_object_or_404()
5.12024Async support in login_required and related decorators, async session API
5.22025Async user manager and permission methods, async auth backends
6.02025Background Tasks framework (with aenqueue()), AsyncPaginator
6.12026No major async additions

The detail that matters most sits in the 4.1 release notes and has not changed since: “the underlying database operations remain synchronous.” The async QuerySet interface “encapsulates the necessary sync_to_async() operations for you” (Django 4.1 notes). There is still no async database backend in 6.1.

So the async ORM is a convenience for writing async views, not a faster database path.

Where async views win

The textbook case is a view that calls several external services and could call them at once. Assume each of these three upstream calls takes around 300ms.

Synchronous version (sequential):

def dashboard_view(request):
    user_stats = fetch_analytics_api(request.user.id)       # waits ~300ms
    notifications = fetch_notification_service(request.user.id)  # then ~300ms
    weather = fetch_weather_api(request.user.city)           # then ~300ms
    # roughly 900ms of waiting, one call after another

    return render(request, "dashboard.html", {
        "stats": user_stats,
        "notifications": notifications,
        "weather": weather,
    })

Async version (concurrent):

import asyncio

async def dashboard_view(request):
    user = await request.auser()  # request.user is not safe to touch in async code

    user_stats, notifications, weather = await asyncio.gather(
        fetch_analytics_api(user.id),         # must be async (e.g. httpx.AsyncClient)
        fetch_notification_service(user.id),
        fetch_weather_api(user.city),
    )
    # the waits overlap, so total time is close to the slowest single call

    return render(request, "dashboard.html", {
        "stats": user_stats,
        "notifications": notifications,
        "weather": weather,
    })

Two things make this work. The fetch functions have to be real coroutines using an async HTTP client, otherwise gather has nothing to overlap. And the user has to come from await request.auser(), which Django added in 5.0 for async contexts (request and response docs), because lazy database access from async code raises SynchronousOnlyOperation.

The async ORM, and why gather does not parallelize it

Django’s async QuerySet methods cover most of what you need: aget(), acreate(), aget_or_create(), acount(), aexists(), afirst(), aaggregate(), aupdate(), adelete(), aiterator() and async for iteration (QuerySet reference). There is no alist(). To evaluate a queryset into a list, use an async comprehension:

async def project_detail(request, project_id):
    project = await Project.objects.select_related("client").aget(id=project_id)

    recent_activity = [
        a async for a in
        Activity.objects.filter(project_id=project_id).order_by("-created_at")[:10]
    ]
    team_members = [
        m async for m in
        TeamMember.objects.filter(project_id=project_id).select_related("user")
    ]

    return render(request, "projects/detail.html", {
        "project": project,
        "activity": recent_activity,
        "team": team_members,
    })

It is tempting to wrap those three queries in asyncio.gather. It won’t make them faster. Each async ORM call goes through sync_to_async with thread_sensitive=True, and the async docs say that “within a single request, multiple thread_sensitive calls serialize on that request’s worker thread.” The queries still run one at a time. Concurrency across requests is fine; concurrency within one request is not.

Watch for lazy relations too. Accessing a foreign key that was not loaded with select_related during async evaluation raises SynchronousOnlyOperation (making queries). Load what the template needs up front.

Transactions and connections

Two more limits from the docs:

  • “Transactions do not yet work in async mode.” The recommendation is to write the transactional part as one synchronous function and call it with sync_to_async().
  • Persistent connections (CONN_MAX_AGE) should be disabled in async mode. Use your database backend’s connection pooling instead; Django has supported PostgreSQL connection pools since 5.1.
from asgiref.sync import sync_to_async
from django.db import transaction

@sync_to_async
def transfer_credits(from_id, to_id, amount):
    with transaction.atomic():
        sender = Account.objects.select_for_update().get(id=from_id)
        receiver = Account.objects.select_for_update().get(id=to_id)
        sender.balance -= amount
        receiver.balance += amount
        sender.save()
        receiver.save()

async def transfer_view(request):
    await transfer_credits(request.POST["from"], request.POST["to"], int(request.POST["amount"]))
    return JsonResponse({"ok": True})

Async middleware, done the documented way

Middleware is where older blog posts often go wrong. Django does not call a separate __acall__ method. The middleware docs describe flags (sync_capable, async_capable) and a pattern where an async-only class marks itself as a coroutine function:

import time
from asgiref.sync import iscoroutinefunction, markcoroutinefunction

class TimingMiddleware:
    async_capable = True
    sync_capable = False

    def __init__(self, get_response):
        self.get_response = get_response
        if iscoroutinefunction(self.get_response):
            markcoroutinefunction(self)

    async def __call__(self, request):
        start = time.monotonic()
        response = await self.get_response(request)
        response["X-Request-Duration"] = f"{time.monotonic() - start:.3f}s"
        return response

For middleware that should support both modes, use the sync_and_async_middleware decorator on a function-style middleware and branch on whether get_response is a coroutine function.

This matters for performance. If an async view sits behind sync-only middleware, Django switches between the event loop and a thread to run it. The docs describe the per-switch cost as small, but note it matters most when you rely on ASGI for high in-process concurrency over non-ORM I/O. Check that the middleware in your stack, including third-party ones, is async capable before judging async performance.

Where async makes things worse

CPU-bound work

Async makes waiting cheaper. It does not make computation faster. A view that spends seconds crunching data blocks the event loop, and every other request on that worker waits:

# This is NOT faster as an async view
async def generate_report(request):
    data = [row async for row in Report.objects.filter(owner_id=request.GET["owner"])]
    report = compute_complex_aggregations(data)  # pure CPU, blocks the loop
    pdf = render_pdf(report)                     # more CPU
    return FileResponse(pdf)

With sync gunicorn workers, one busy worker does not stall the others. On an async worker, it does. Better options are a sync view, the Django Tasks framework for work that can finish in the background, or moving the computation off the loop:

async def generate_report(request):
    data = [row async for row in Report.objects.filter(owner_id=request.GET["owner"])]
    report = await asyncio.to_thread(compute_complex_aggregations, data)
    pdf = await asyncio.to_thread(render_pdf, report)
    return FileResponse(pdf)

asyncio.to_thread keeps the loop free, but on the standard CPython build threads still share the GIL for pure Python code, so this protects latency for other requests more than it speeds up the report itself. Heavy model inference is a good example of work that belongs in sync workers or a separate service; our post on Django as an AI backend covers that pattern.

The sync_to_async trap

Plenty of libraries are synchronous, including many payment and vendor SDKs. Calling one from an async view means wrapping it:

from asgiref.sync import sync_to_async

async def process_payment(request):
    result = await sync_to_async(payments_client.create_charge)(
        amount=1000,
        currency="usd",
        customer_id=request.POST["customer_id"],
    )
    return JsonResponse({"charge_id": result.id})

That works. But sync_to_async runs the function in a worker thread, and with the default thread_sensitive=True all of a request’s thread-sensitive calls share one thread. If a view makes three or four such calls in a row, you have the overhead of an async view plus thread handoffs, and no overlap at all. A plain sync view does the same work with less machinery.

Before wrapping a library, check whether it offers an async client. Many HTTP-based SDKs now do. If it does not, and the view is mostly sync calls, keep the view sync.

Third-party packages

Async support in the Django ecosystem is uneven. Django REST framework, the most common example, does not ship async views: async does not appear in its release notes through 3.18. The third-party adrf package adds async views and viewsets if you need them. For any package in a hot path, check whether it calls the ORM or other sync code internally before assuming an async view will help. If you are comparing frameworks for an async-heavy API, our Django REST framework vs FastAPI comparison covers that tradeoff.

ASGI vs. WSGI: staying on WSGI is fine

You need ASGI to run async views as async. You do not need async views for most Django apps.

AspectWSGI (gunicorn)ASGI (uvicorn, daphne)
Async viewsRun, but without concurrency benefitsYes
Sync viewsNativeYes, via a thread
Worker modelPre-fork processes, simple and predictableEvent loop per worker
WebSocketsNoYes (with Channels)
Long-lived streaming / SSETies up a worker per connectionSuited to it
Failure modeA stuck worker affects its own requestA blocked loop affects every connection on that worker

If you are not using async views for concurrent I/O, WebSockets or streaming, stay on WSGI. If you are, Django’s Uvicorn deployment guide recommends gunicorn as the process manager with the uvicorn-worker package. The old uvicorn.workers module is deprecated according to Uvicorn’s docs.

# WSGI: still a good default for most Django projects
gunicorn myproject.wsgi:application --workers 4 --threads 2 --timeout 30

# ASGI: when you actually need async
python -m pip install uvicorn uvicorn-worker gunicorn
python -m gunicorn myproject.asgi:application -k uvicorn_worker.UvicornWorker --workers 4

A decision rule you can apply per view

Decide view by view, not for the whole project. Count the independent waits in a request: HTTP calls to other services, remote cache lookups, anything that leaves the process and that could run at the same time as the others.

  • Zero or one independent wait: stay sync. There is nothing to overlap.
  • Two or three: consider async if each wait is slow and you can use async clients for them.
  • Four or more slow, independent waits: async is likely to help, and the gain grows with the number of overlapping calls.

Do not count ORM queries as independent waits within a request, for the reason covered above. And if the view needs a transaction, the transactional part stays sync regardless.

Then measure. Load test the sync and async versions of that one view in staging with realistic upstream latency before you switch, because the outcome depends on your middleware, your libraries and your database far more than on any general rule.

The bottom line: async Django in 2026 is a precision tool. Use it for views that wait on several slow services at once, and for WebSockets and streaming. For everything else, synchronous Django on gunicorn is still the boring, correct answer. If you are hiring someone to make this call on your codebase, our guide to hiring a Python developer covers how to test for it.


This is the third post in our Django in 2026 series. Previously: Django + HTMX. Next up: Django as your AI backend. For what changed in the latest release, see Django 6.1: fetch modes, FK deletes and MAILERS.

Frequently asked questions

Is async Django production-ready in 2026?
Yes, for the right workload. Async views, middleware, the async ORM interface, async auth helpers and async signals are all supported, and Django 6.1 is the current release. But the database layer is still synchronous underneath and transactions don't work in async mode, so async helps mainly with views that wait on several external network calls at once.
Does asyncio.gather run Django ORM queries in parallel?
Not in a meaningful way. Django's async QuerySet methods wrap sync_to_async with thread_sensitive=True, and Django's docs say thread-sensitive calls within a single request serialize on that request's worker thread. Gathering three ORM calls in one view still runs them one after another. Gathering three HTTP calls made with an async client does overlap them.
Does async Django give me more database connections?
No. The async ORM uses the same synchronous database layer and the same connections. Django's async docs also say persistent connections (CONN_MAX_AGE) should be disabled in async mode, and suggest using your database backend's connection pooling instead.
What is the sync_to_async trap in Django?
sync_to_async runs a synchronous function in a worker thread so an async view can await it. If most of a view's work goes through sync_to_async (sync SDKs, sync ORM helpers, sync auth code), you pay for the thread handoff on every call without gaining any concurrency, and a plain sync view is simpler and usually no slower.
Should I switch from WSGI to ASGI for my Django app?
Only if you need async views for concurrent I/O, WebSockets, or long-lived streaming responses. Gunicorn on WSGI is simpler to operate and debug. If you do move to ASGI, Django's deployment docs recommend running gunicorn with the uvicorn-worker package's UvicornWorker.
Does Django REST framework support async views?
Not officially. The DRF release notes through 3.18 do not add async view support. The third-party adrf package adds async views and viewsets for DRF if you need them.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored