In Django, why does an async view that calls Invoice.objects.get() raise SynchronousOnlyOperation, and how do you fix it?
answer
- a running loop in this thread
- the database layer is guarded
- not only inside async def
- an environment variable is not a fix
basics
~20 sDjango guards its database layer as async-unsafe: when a guarded call runs in a thread with a running event loop, it raises SynchronousOnlyOperation. Use the async ORM API or wrap the sync code in sync_to_async; never DJANGO_ALLOW_ASYNC_UNSAFE.
solid answer
~40 s`Invoice.objects.get()` is synchronous: it opens a cursor on the thread's database connection. Django marks those connection methods with an async-unsafe decorator that checks for a **running event loop in the current thread** and raises `SynchronousOnlyOperation` with "You cannot call this from an async context - use a thread or sync_to_async." In an async view the loop is running, so the call is refused — which protects the per-thread connection from being shared by coroutines. Fix it with the async ORM (`await Invoice.objects.aget(...)`) or by moving the sync work into a function you `await sync_to_async(...)`. `DJANGO_ALLOW_ASYNC_UNSAFE` disables the guard; it is meant for notebooks, and `check --deploy` reports it as `async.E001`.
code
python · 23 linesfrom asgiref.sync import sync_to_async
from django.http import JsonResponse
from billing.models import Invoice
async def invoice_broken(request, pk):
invoice = Invoice.objects.get(pk=pk) # SynchronousOnlyOperation
return JsonResponse({'total': str(invoice.total)})
async def invoice_async_orm(request, pk):
invoice = await Invoice.objects.aget(pk=pk)
return JsonResponse({'total': str(invoice.total)})
def _load_total(pk):
return str(Invoice.objects.get(pk=pk).total)
async def invoice_wrapped(request, pk):
total = await sync_to_async(_load_total)(pk)
return JsonResponse({'total': total})go deeper
Recognise the error as calling sync ORM code from async code, and know the two fixes: the a-prefixed ORM methods or sync_to_async.
Explain that the check is per thread with a running loop, which is why sync helpers and notebooks trip it too.
Trace from the traceback to the triggering line, including lazy evaluation, and explain why DJANGO_ALLOW_ASYNC_UNSAFE and async.E001 rule out the shortcut.
Treat the guard as a safety net worth keeping: codify that no deployment sets the variable and that sync database work crosses a deliberate boundary.
## What the error protects Django keeps **one database connection per thread**. In async code many coroutines share one thread, so if two of them used that connection at once their queries and transactions could interleave and corrupt data. Django therefore classifies parts of itself as **async-unsafe** and refuses to run them in an async context. The refusal is `django.core.exceptions.SynchronousOnlyOperation`, and its default message is: *"You cannot call this from an async context - use a thread or sync_to_async."* ## How the guard decides The guard is a decorator, `async_unsafe`, from `django.utils.asyncio`. In current Django it wraps the database backend's connection-level methods — `connect()`, `ensure_connection()`, `cursor()`, `commit()`, `rollback()`, `close()` and the savepoint methods. Each call: 1. asks whether **this thread has a running event loop**; 2. if not, runs normally; 3. if so, and `DJANGO_ALLOW_ASYNC_UNSAFE` is not set, raises `SynchronousOnlyOperation`. Because the check is about the **thread**, not the syntax, you can trip it without writing `async def` near the call: - a sync helper called directly from an async view runs on the loop's thread; - a Jupyter notebook or IPython shell keeps a loop running in the main thread, so plain ORM calls there fail too (IPython's `%autoawait off` turns that loop off). ## Fixes, in order of preference | Situation | Fix | |---|---| | Simple reads and writes | The async ORM API: `aget()`, `acreate()`, `async for` | | Several ORM calls that belong together, or a transaction | One sync function, called once with `await sync_to_async(fn)(...)` | | A sync-only library that uses the ORM internally | Wrap the library call with `sync_to_async` | | Interactive shell with an auto-running loop | `%autoawait off` in IPython, or use the async API with `await` | ## Why the environment variable is the wrong fix Setting `DJANGO_ALLOW_ASYNC_UNSAFE` (to any non-empty value) makes the guard skip its check. The docs allow it only when you are **certain** nothing runs concurrently — a notebook session, a one-off script — and warn that concurrent access can cause **data loss or corruption**. - The system check framework registers a deploy check: `manage.py check --deploy` reports **`async.E001`**, "You should not set the DJANGO_ALLOW_ASYNC_UNSAFE environment variable in deployment." - It hides the real design problem: the view is doing sync database work on the event loop's thread, which also blocks every other coroutine while the query runs. ## Reading the traceback 1. The bottom frames are inside the database backend (`cursor()`, `ensure_connection()`). 2. Walk up to the first frame in your code; that line is where the sync path started. 3. If that line does not look like a query, a lazy object is evaluating one — a related attribute, a deferred field, a QuerySet being iterated. 4. Choose the fix by situation from the table above. ## Where the guard shows up outside views - **Async tests** that call a sync fixture helper directly from an `async def` test body. - **Callbacks** handed to an async library that invokes them on its loop's thread, when the callback touches the ORM. - **Scripts** that start a loop with `asyncio.run()` and then call ordinary ORM helpers from inside a coroutine. In every case the diagnosis is the same: a guarded database call ran on a thread that currently runs an event loop. ## A common confusion The guard only fires in a thread **with a running loop**. Code running inside `sync_to_async` executes on a worker thread with no loop, so the same `Invoice.objects.get()` call is legal there. That is precisely why wrapping works — the adapter moves the call to a thread where a per-thread connection is safe to use.
- Why does a plain ORM query fail with SynchronousOnlyOperation in a Jupyter notebook that has no async code?Jupyter and IPython keep an event loop running in the main thread so that top-level `await` works. The guard checks for a running loop in the current thread, finds one, and refuses. Run `%autoawait off` in IPython to stop that loop, or use the async ORM with `await`.
- Is it acceptable to set DJANGO_ALLOW_ASYNC_UNSAFE in a production container to silence the error?No. It disables the protection for every request, and concurrent coroutines can then share a thread's connection, risking data loss or corruption. `manage.py check --deploy` flags it as `async.E001`. Fix the call site with the async API or `sync_to_async` instead.
saying these in an interview costs you the question
- SynchronousOnlyOperation only fires for code written inside an async def body.
- Setting DJANGO_ALLOW_ASYNC_UNSAFE is the standard fix for async views.
- The error means the query was too slow for the event loop.
- Calling the sync ORM inside a sync_to_async-wrapped helper raises the same error.
- Jupyter fails because notebooks cannot open database connections.