Async Views & ASGI

Reviewed & published by Brayan K

An async view is a view defined with async def that can await coroutines, letting the server do other work while it waits on slow input or output such as a database query or an external API call.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll write async def views, see exactly when async helps by running calls concurrently with asyncio.gather, bridge sync and async with sync_to_async, touch the async ORM, and run the project under an ASGI server.

What You'll Learn in This Lesson

1 Writing an async def View

An async view looks almost identical to a normal one — you just write async def and use await for any coroutine it calls. Django recognizes the coroutine and runs it on the event loop. The win is that while the view is awaiting slow input or output, the server isn't blocked and can make progress on other requests.

# views.py
from django.http import JsonResponse
import httpx

async def profile_view(request, user_id):
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"https://api.example.com/users/{user_id}")
    return JsonResponse(resp.json())

# urls.py — wired up exactly like a sync view
from django.urls import path
urlpatterns = [
    path("profile/<int:user_id>/", profile_view),
]
# An async view is just a coroutine: "async def view(request)". When it
# awaits I/O (a DB query, an API call), the event loop can run OTHER work
# instead of blocking. This simulates an async view awaiting a slow call.

import asyncio
import time

async def fetch_profile(user_id):
    # pretend this is an awaited HTTP/DB call that takes time
    await asyncio.sleep(0.05)
    return {"id": user_id, "name": f"User{user_id}"}

async def profile_view(request):
    # this is what "async def view(request)" looks like
    data = await fetch_profile(request["user_id"])
    return f"200 OK: {data}"

async def main():
    start = time.perf_counter()
    result = await profile_view({"user_id": 42})
    elapsed = time.perf_counter() - start
    print(result)
    print(f"took about {elapsed:.2f}s (one awaited call)")

asyncio.run(main())

# EXPECTED:
# 200 OK: {'id': 42, 'name': 'User42'}
# took about 0.05s (one awaited call)

2 When Async Helps — Concurrency with gather

Async pays off when a view makes several independent I/O calls. With asyncio.gather you launch them all at once and await them together, so they overlap instead of running one after another. Three 100ms calls finish in about 100ms total. For CPU-heavy work or a single quick query, async gives little or no benefit.

# views.py — fan out to three services concurrently
import asyncio
from django.http import JsonResponse

async def dashboard_view(request):
    user, orders, recs = await asyncio.gather(
        get_user(request.user.id),
        get_orders(request.user.id),
        get_recommendations(request.user.id),
    )
    return JsonResponse({"user": user, "orders": orders, "recs": recs})
# Where async SHINES: many independent I/O calls at once. asyncio.gather
# runs the awaitables CONCURRENTLY, so three 0.1s calls finish in ~0.1s
# total instead of ~0.3s. This is the whole reason to use async views.

import asyncio
import time

async def call_api(name, delay):
    await asyncio.sleep(delay)   # awaited I/O
    return f"{name} done"

async def sequential():
    start = time.perf_counter()
    a = await call_api("A", 0.1)
    b = await call_api("B", 0.1)
    c = await call_api("C", 0.1)
    return time.perf_counter() - start, [a, b, c]

async def concurrent():
    start = time.perf_counter()
    results = await asyncio.gather(
        call_api("A", 0.1),
        call_api("B", 0.1),
        call_api("C", 0.1),
    )
    return time.perf_counter() - start, results

async def main():
    seq_time, seq = await sequential()
    con_time, con = await concurrent()
    print("sequential results:", seq)
    print(f"sequential time: ~{seq_time:.1f}s")
    print("concurrent results:", con)
    print(f"concurrent time: ~{con_time:.1f}s  (gather ran them at once)")

asyncio.run(main())

# EXPECTED:
# sequential results: ['A done', 'B done', 'C done']
# sequential time: ~0.3s
# concurrent results: ['A done', 'B done', 'C done']
# concurrent time: ~0.1s  (gather ran them at once)

3 Bridging Sync/Async, the Async ORM, and ASGI

Real apps mix worlds. sync_to_async wraps blocking code so an async view can await it; async_to_sync runs a coroutine from sync code. Since Django 4.1 the ORM has async methods — the same queries with an a prefix, like afirst, acreate, and async for. To run async views with full concurrency, serve the project through asgi.py with an ASGI server like Uvicorn or Daphne.

# views.py — async ORM and a wrapped blocking call
from asgiref.sync import sync_to_async
from .models import Post

async def latest_post_view(request):
    post = await Post.objects.afirst()              # async ORM
    await Post.objects.acreate(title="Hello async") # async create

    async for p in Post.objects.all():              # async iteration
        print(p.title)

    legacy = await sync_to_async(some_blocking_lib)()  # wrap blocking code
    return JsonResponse({"latest": post.title if post else None})
# Run it under an ASGI server (not the WSGI one):
# myproject/asgi.py is created for you by startproject
uvicorn myproject.asgi:application --reload
# or
daphne myproject.asgi:application
# Bridging sync and async. sync_to_async wraps blocking code so an async
# view can await it; async_to_sync runs a coroutine from sync code. We
# simulate the wrappers and the async ORM's "a" methods (afirst/acreate).

import asyncio

def blocking_legacy_call():
    # imagine this touches a sync-only library
    return "legacy result"

def sync_to_async(fn):
    async def wrapper(*args, **kwargs):
        # really this runs fn in a thread; we just await a tick
        await asyncio.sleep(0)
        return fn(*args, **kwargs)
    return wrapper

# Pretend ORM: the async API mirrors the sync one with an "a" prefix
class AsyncQuerySet:
    def __init__(self, rows):
        self.rows = rows
    async def afirst(self):
        await asyncio.sleep(0)
        return self.rows[0] if self.rows else None
    async def acreate(self, **kwargs):
        await asyncio.sleep(0)
        self.rows.append(kwargs)
        return kwargs

async def async_view(request):
    # await a wrapped blocking call
    legacy = await sync_to_async(blocking_legacy_call)()
    # await the async ORM
    qs = AsyncQuerySet([{"id": 1, "title": "First"}])
    first = await qs.afirst()
    created = await qs.acreate(id=2, title="New")
    return legacy, first, created

legacy, first, created = asyncio.run(async_view({}))
print("sync_to_async ->", legacy)
print("await qs.afirst() ->", first)
print("await qs.acreate() ->", created)

# EXPECTED:
# sync_to_async -> legacy result
# await qs.afirst() -> {'id': 1, 'title': 'First'}
# await qs.acreate() -> {'id': 2, 'title': 'New'}

# ✅ Expected output:
# sync_to_async -> legacy result
# await qs.afirst() -> {'id': 1, 'title': 'First'}
# await qs.acreate() -> {'id': 2, 'title': 'New'}

🎯 Your Turn: Run Two Calls at Once

Replace the blank with asyncio.gather(...) so the two awaited tasks run concurrently and return a list of results.

# 🎯 YOUR TURN: run two awaited calls CONCURRENTLY with asyncio.gather.
# Replace ___ with asyncio.gather(task_a(), task_b()).

import asyncio

async def task_a():
    await asyncio.sleep(0.01)
    return "A"

async def task_b():
    await asyncio.sleep(0.01)
    return "B"

async def main():
    results = await ___
    print("results:", results)

asyncio.run(main())

# EXPECTED OUTPUT:
# results: ['A', 'B']

Common Errors (And How to Fix Them)

❌ SynchronousOnlyOperation in an async view

You called the sync ORM directly inside async def.

✅ Fix: use the async methods (afirst, acreate, async for) or wrap in sync_to_async.

❌ My async view isn't any faster

You awaited calls one at a time, so they ran sequentially.

✅ Fix: launch independent calls together with asyncio.gather(...).

❌ "coroutine was never awaited" warning

You called an async function without await, so it never ran.

✅ Fix: await the coroutine, or pass it to gather.

📋 Quick Reference

ItemWhat It Does
async def view(request)Define an async view that can await coroutines
asyncio.gather(...)Run several awaitables concurrently
sync_to_async(fn)Wrap blocking code so you can await it
Post.objects.afirst()Async ORM method (a-prefixed)
uvicorn myproject.asgi:applicationServe the app under an ASGI server

🎯 Mini Challenge: Build an Async Dashboard View

Write an async view that fans out to three services with gather, combines the results, and shows that the three 100ms calls finish in about 100ms total.

# 🎯 Mini Challenge: an async view that fans out to several services at
# once with gather, then combines the results. The three 0.1s calls run
# concurrently, so the whole view finishes in ~0.1s.

import asyncio
import time

async def get_user(uid):
    await asyncio.sleep(0.1)
    return {"id": uid, "name": f"User{uid}"}

async def get_orders(uid):
    await asyncio.sleep(0.1)
    return [f"order-{uid}-1", f"order-{uid}-2"]

async def get_recommendations(uid):
    await asyncio.sleep(0.1)
    return ["book", "course"]

async def dashboard_view(uid):
    start = time.perf_counter()
    user, orders, recs = await asyncio.gather(
        get_user(uid),
        get_orders(uid),
        get_recommendations(uid),
    )
    elapsed = time.perf_counter() - start
    return {
        "user": user,
        "orders": orders,
        "recommendations": recs,
        "elapsed": round(elapsed, 1),
    }

page = asyncio.run(dashboard_view(7))
print("user:", page["user"])
print("orders:", page["orders"])
print("recommendations:", page["recommendations"])
print("elapsed ~", page["elapsed"], "s (3 x 0.1s calls ran concurrently)")

# EXPECTED:
# user: {'id': 7, 'name': 'User7'}
# orders: ['order-7-1', 'order-7-2']
# recommendations: ['book', 'course']
# elapsed ~ 0.1 s (3 x 0.1s calls ran concurrently)

❓ Frequently Asked Questions

Lesson complete — you can write concurrent views!

You wrote async def views, saw exactly when async helps by overlapping I/O with asyncio.gather, bridged worlds with sync_to_async, used the async ORM's a-prefixed methods, and learned to serve the project under an ASGI server.

Practice quiz

How do you define an async view in Django?

  • def view(request): with @async
  • async view(request):
  • view = async(request)
  • async def view(request):

Answer: async def view(request):. An async view is simply written with async def and can await coroutines.

What does asyncio.gather let you do?

  • Run several awaitables concurrently
  • Block the event loop
  • Cache responses
  • Define a model

Answer: Run several awaitables concurrently. gather launches awaitables together so independent I/O calls overlap.

When does async give the biggest speedup?

  • Heavy CPU computation
  • A single quick query
  • Several independent I/O calls run concurrently
  • Rendering a static page

Answer: Several independent I/O calls run concurrently. Async helps most when overlapping multiple independent I/O-bound calls.

What does sync_to_async do?

  • Speeds up CPU work
  • Wraps blocking code so an async view can await it
  • Converts a model to JSON
  • Starts an ASGI server

Answer: Wraps blocking code so an async view can await it. sync_to_async wraps a blocking function so it can be awaited without freezing the loop.

Which is a correct async ORM method added in Django 4.1?

  • Post.objects.first_async()
  • Post.objects.get_async()
  • Post.objects.async_first()
  • Post.objects.afirst()

Answer: Post.objects.afirst(). The async ORM mirrors sync methods with an 'a' prefix, e.g. afirst, acreate.

What error can the sync ORM raise inside an async view?

  • SynchronousOnlyOperation
  • AsyncRequiredError
  • EventLoopError
  • BlockingIOError

Answer: SynchronousOnlyOperation. Calling the sync ORM directly in async def can raise SynchronousOnlyOperation.

Which protocol replaces WSGI for async Django?

  • FastCGI
  • ASGI
  • HTTP/3
  • gRPC

Answer: ASGI. ASGI is the async successor to WSGI and supports long-lived connections.

Which command serves the project under an ASGI server?

  • python manage.py runwsgi
  • gunicorn myproject.wsgi
  • uvicorn myproject.asgi:application
  • python manage.py asgi

Answer: uvicorn myproject.asgi:application. An ASGI server like Uvicorn or Daphne serves the asgi:application callable.

Why might an async view be no faster than a sync one?

  • You awaited calls one at a time instead of using gather
  • You used JsonResponse
  • You forgot to import asyncio
  • You used too few URLs

Answer: You awaited calls one at a time instead of using gather. Awaiting sequentially runs calls one after another; use gather to overlap them.

What causes a 'coroutine was never awaited' warning?

  • Using gather
  • Defining async def
  • Serving under ASGI
  • Calling an async function without await

Answer: Calling an async function without await. Calling a coroutine without await (or gather) means it never runs.

Continue this course

Frequently asked questions

What is an async view in Django?

An async view is a view defined with async def instead of plain def. Inside it you can use await to call coroutines, which lets the server do other work while that view is waiting on slow input or output, such as a database query or an external API call. Django detects that the view is a coroutine and runs it on the event loop. Async views were added in Django 3.1 and the async ORM that makes them genuinely useful arrived in Django 4.1.

When does async actually make my Django site faster?

Async helps when a view spends most of its time waiting on input or output that can happen concurrently — calling several external APIs, making multiple independent database queries, or streaming. By awaiting those calls with asyncio.gather they overlap, so three 100ms calls finish in about 100ms instead of 300ms. Async does not speed up CPU-bound work like heavy computation, and a view that makes a single quick query gains almost nothing, so reach for it only where concurrency pays off.

What do sync_to_async and async_to_sync do?

They bridge synchronous and asynchronous code. sync_to_async wraps a blocking function so you can await it from an async view without freezing the event loop — Django runs it in a thread. async_to_sync does the opposite: it lets ordinary synchronous code call and wait for a coroutine. You need these adapters because mixing the two worlds directly raises errors, and a lot of Django and third-party code is still synchronous.

What is ASGI and how do I run async views?

ASGI is the Asynchronous Server Gateway Interface, the async successor to WSGI. It is the protocol that lets a server speak to an async Django application and also handles long-lived connections like WebSockets. To actually run async views with full concurrency you serve the project through your asgi.py file with an ASGI server such as Uvicorn or Daphne, for example 'uvicorn myproject.asgi:application'. Under the development server async views still work, but you get the most benefit under a real ASGI server.