In Django, what does await Notification.objects.aget(pk=pk) actually do underneath, and what does that mean for performance?
answer
- read the method body
- the driver is still synchronous
- same save, same signals
- a thread per call
basics
~10 sIt awaits sync_to_async(self.get): the ordinary synchronous get() runs on a worker thread with a synchronous database driver. The async ORM frees the event loop, but each query still occupies a thread and a connection.
solid answer
~40 sThe async ORM is an **interface**, not an async database stack. In current Django `aget()` is literally `await sync_to_async(self.get)(...)`, and the same pattern holds for `acount()`, `acreate()`, `asave()`, `aadd()` and the rest. The 4.1 release notes say the underlying database operations remain synchronous and the interface encapsulates the `sync_to_async()` calls for you. Consequences: the event loop stays free while the query runs, but the query runs on a thread with the usual driver and per-thread connection; overridden `save()` methods and `pre_save`/`post_save` receivers still run, synchronously, in that thread; and a loop of a-calls is a loop of thread handoffs. The async ORM makes Django code correct in async views — it does not make queries faster.
code
python · 21 lines# Paraphrased shape of the implementation in Django 6.1
from asgiref.sync import sync_to_async
# django/db/models/query.py
class QuerySet:
async def aget(self, *args, **kwargs):
return await sync_to_async(self.get)(*args, **kwargs)
async def acount(self):
return await sync_to_async(self.count)()
# django/db/models/base.py
class Model:
async def asave(self, *, force_insert=False, force_update=False,
using=None, update_fields=None):
return await sync_to_async(self.save)(
force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields,
)go deeper
Know that the a-prefixed methods exist so async views can query safely, not because the database got faster.
Explain that each a-method wraps its sync twin with sync_to_async, so the sync driver, save() overrides and signals all still run.
Reason about throughput: handoffs per call, serialized thread-sensitive calls, pool-bound capacity, and why gather over a-calls is not parallel.
Set expectations for an async migration: the ORM interface buys correctness today and future-proofing, while database throughput still comes from query design.
## What the source says Open `django/db/models/query.py` and the async methods are short: - `aget()` returns `await sync_to_async(self.get)(*args, **kwargs)`; - `acount()`, `aexists()`, `acreate()`, `aget_or_create()`, `aupdate()`, `adelete()` follow the same shape; - `Model.asave()` in `base.py` awaits `sync_to_async(self.save)` with the same keyword arguments; - related-manager methods such as `aadd()` and `acreate()` wrap their sync counterparts the same way. The 4.1 release notes, which introduced the interface, are explicit: the underlying database operations **remain synchronous**, and the new interface "encapsulates the necessary `sync_to_async()` operations for you", so code written against it can benefit later if async support is pushed down into the SQL compiler and async drivers. ## What that implies | Question | Answer in current Django | |---|---| | Does the query block the event loop? | No — it runs on a worker thread while the loop serves other coroutines | | Does it use an async database driver? | No — the same synchronous driver and per-thread connection | | Does an overridden `save()` run on `asave()`? | Yes — `asave()` calls `self.save()` | | Do `pre_save` / `post_save` receivers fire? | Yes, synchronously, inside that thread | | Is it faster than a sync view doing the same query? | Not per query; the SQL and driver are identical, plus a small handoff | | Can two a-calls in one request overlap? | Generally no — they are thread-sensitive, so they run one after another on the request's worker | ## Performance consequences 1. **Correctness, not speed.** The async ORM lets an async view query the database without tripping the async-safety guard or stalling the loop. Each query costs what it always did. 2. **Handoffs add up.** Every a-call is one crossing to a thread and back. In a loop over hundreds of rows those crossings — and the queries — multiply; fetch in bulk instead. 3. **Gathering a-calls does not parallelise them.** Wrapping five `acount()` calls in `asyncio.gather` looks concurrent, but thread-sensitive calls in one request serialize on its worker. Overlap comes from non-database awaits running beside them. 4. **Connections are still per thread.** Capacity is bounded by the database connection pool, not by how many coroutines you can create. ## Why use it anyway - It is the **supported** way to query from async code, and it keeps views free of hand-written adapter calls. - It gives the right shape for the future: if Django later runs queries natively async, code written with `aget()` benefits without changes. - It combines naturally with the parts that *are* worth doing async — awaiting several HTTP calls, streaming, long-polling — in the same view. ## Where the async ORM still helps performance The async ORM does not speed up a query, but it can speed up a **request**: - an async view can `await` a database lookup and, alongside it, awaited HTTP calls or cache reads that genuinely run concurrently on the loop; - under ASGI, a request waiting on those network calls holds a coroutine rather than a thread, so the process sustains more concurrent requests; - database-bound time is unchanged, so the gain is proportional to how much of the request is spent **outside** the database. The practical sizing rule follows: pool size and query count bound database throughput, whatever style the view is written in. ## What an interviewer is checking They want to hear that you have read past the name. A candidate who says "`aget()` uses an async driver so it is faster" has the model backwards; a strong answer names the `sync_to_async` wrapper, the synchronous driver, the per-thread connection, and the practical rule that async ORM calls are for correctness inside async code, while throughput still comes from fewer and better queries.
- If you override save() on a Django model, does asave() run your override?Yes. `Model.asave()` awaits `sync_to_async(self.save)`, so your overridden `save()` runs, on a worker thread, together with the `pre_save` and `post_save` signals it sends. Overriding only `asave()` would not affect sync callers, so put custom save logic in `save()`.
- Will asyncio.gather over five acount() calls in one Django request run the counts in parallel?No. Each `acount()` is a thread-sensitive `sync_to_async` call, and within one request those calls serialize on the request's worker thread. The gather returns when all five have run one after another; to save time, combine them into one aggregate query.
A receptionist who takes your request and hands it to the same back-office clerk as before: you can wait in the lobby instead of blocking the desk, but the clerk works at the same speed.
saying these in an interview costs you the question
- aget() uses an async database driver, so it is faster than get().
- asave() skips overridden save() methods and model signals.
- Gathering several a-prefixed ORM calls runs the queries in parallel.
- The async ORM removes the need for database connections per thread.
- Switching views to a-prefixed calls reduces database load.