RizTech Academy logo
RizTech Academy
Concurrency and Background WorkLesson 1 of 435 min

Async views and the ASGI story: when they help

Django began as a synchronous framework — one request, handled start to finish, on one worker. Since Django 3.1 it also supports async views, and Django 5 has a mature async story: async views, async ORM methods, async middleware. But async is one of the most misunderstood features in the framework — it helps a specific kind of workload and does nothing (or harms) for the rest. This lesson is what async views are, when they genuinely help, and the honest answer to "should I make everything async?" (no).

What an async view looks like

An async view is a coroutine — async def instead of def — and it uses the async ORM methods:

from django.http import JsonResponse
from patients.models import Patient

async def patient_count(request):
    n = await Patient.objects.acount()        # async ORM method (note the 'a' prefix)
    return JsonResponse({"patients": n})

Verified: this async view returns HTTP 200 with the count. Django detects the async def and runs it on the async path. The ORM provides async versions of its methods with an a prefix — acount(), aget(), afirst(), acreate(), and async for over a queryset — because the database call must be awaited. You run async views under an ASGI server (the asgi.py from setup) rather than WSGI; runserver handles both in development.

The one thing async is actually for: waiting on I/O

Here is the mental model that resolves all the confusion. A synchronous worker, while waiting for something slow — an external HTTP call, another service, a slow query — is blocked, doing nothing but occupying a worker that could serve another request. Async lets a single worker handle other requests while waiting, because await yields control during the wait. So async helps when your view spends its time waiting on I/O it does not control, and you have many such requests at once:

  • Calling several external APIs and waiting on all of them (a view that fans out to a payment gateway, an SMS provider, and a lab system).
  • Proxying or aggregating slow upstream services.
  • Long-lived connections (websockets, server-sent events, streaming) — where a synchronous worker held open per connection would exhaust your workers.

For these, async lets one process juggle many concurrent waits instead of one worker per wait. That is the genuine win, and it is real.

What async does NOT speed up

The crucial corrections, because this is where people waste effort or make things worse:

  • Async does not make CPU-bound work faster. A view that computes something heavy (a report, image processing) is limited by the CPU, not by waiting — async gives it nothing, because there is no I/O wait to overlap. Async is about waiting efficiently, not computing faster.
  • Async does not speed up a single database query. Your ordinary CRUD view that does a couple of queries and renders a template is already fast and spends almost no time waiting — making it async adds complexity for no benefit. The database round-trip within your own network is not the kind of slow, external wait async is for.
  • A little async in a sync view can be worse than none. Mixing blocking calls into an async view (or the reverse) without care can block the event loop and reduce throughput. Async is not a free upgrade you sprinkle on.

The honest summary: most Django views should stay synchronous. The typical clinic page — list patients, show an appointment, submit a form — is sync, simple, and fast. Async earns its place only for views dominated by concurrent external I/O.

The rule for choosing

A short decision procedure:

  1. Does this view spend most of its time waiting on external I/O (other services, slow upstreams, long-lived connections)? If no → keep it synchronous.
  2. Do you have many such requests concurrently, enough that blocked workers are a real bottleneck? If no → keep it synchronous.
  3. Only if both are yes → an async view may genuinely help, and then make it thoroughly async (async ORM methods, async HTTP client), not half-and-half.

For most applications the answer to step 1 is "no" for almost every view, which is why "keep it sync" is the right default. When you do have the external-I/O-bound, high-concurrency case, async is a powerful tool — use it there, deliberately, and leave the rest of your app synchronous.

Where heavy work really belongs: not async, but a task queue

A common wrong turn: reaching for async to handle slow work like generating a big report or sending emails. Async does not help there — the work is still slow, you have just made the view wait for it differently. The right tool for slow work triggered by a request is a background task queue (the next lesson): the view hands the work off and returns immediately, and a separate worker does it. Async is for handling many concurrent waits efficiently within a request; a task queue is for moving slow work out of the request entirely. Confusing the two is the most common async mistake — knowing which problem you have is the whole skill.

Check your work

What an async view is. An async def view using async ORM methods (acount, aget, async for), run under ASGI. Verified: returns 200 with the count via await Patient.objects.acount().

What async is for. Handling requests that spend their time waiting on external I/O (other services, slow upstreams, long-lived connections) when many happen concurrently — one worker juggles many waits.

What async does not do. It does not speed up CPU-bound work, does not speed up ordinary fast DB/CRUD views, and mixing blocking calls into async can hurt throughput.

The default. Most Django views should stay synchronous — the typical CRUD/page view is already fast and gains nothing from async.

The decision. Async only when the view is external-I/O-bound and highly concurrent; then make it thoroughly async.

Async versus a task queue. Async handles concurrent waits within a request; slow work triggered by a request belongs in a background task queue, not an async view.

Practice

  1. Write an async patient_count view using await Patient.objects.acount(); confirm it returns 200 with the count.
  2. Rewrite an ordinary CRUD list view as async and reason honestly about whether it gained anything (it did not) — feel why "async everything" is wrong.
  3. Sketch a view that calls three external services and explain how async lets one worker handle other requests while it waits.
  4. Take a CPU-heavy operation and explain why making its view async does not speed it up.
  5. For three Nidaan views, decide sync or async and justify each against the two-question rule.
  6. Identify a slow operation (generating a report) and argue why it belongs in a task queue, not an async view.

Official documentation

Next: transactions, select_for_update and the lost-update race.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship