Avoiding SynchronousOnlyOperation in Django¶
SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async. is the error almost everyone meets in their first async Django view. It is Django refusing to run a blocking database call on the event loop thread, where it would stall every other request on the worker. The error is a guard, not a bug, and the fix depends on which of several code paths triggered it — some obvious, like calling .get(), and some hidden, like touching a foreign key. Eleven common patterns were run from async code against Django 6.1.1 on Python 3.14 and PostgreSQL 17. Six raised the error: a sync get(), a plain for over a queryset, len() of a queryset, a lazy foreign key (book.author.name), a reverse relation iterated with for, and a sync helper function that queried — while select_related, prefetch_related, async for and sync_to_async all worked. Setting DJANGO_ALLOW_ASYNC_UNSAFE=true made the errors disappear by running the queries on the loop: five 200 ms queries blocked the event loop for 1,053 ms. This guide finds each trigger and fixes it properly.
Prerequisites¶
- Django 4.1+ for the async queryset API (tested on 6.1.1).
- The async ORM, from using the async Django ORM.
- sync_to_async and threads, from running blocking SDK calls with asyncio.to_thread.
1. Replace direct sync calls with a-methods¶
The direct cases are the easy ones: any queryset method that executes SQL has an async twin, and iteration has async for:
# Each of these raised SynchronousOnlyOperation in an async view
author = Author.objects.get(pk=1)
titles = [b.title for b in Book.objects.all()[:3]]
n = len(Book.objects.all()[:3])
# The async equivalents
author = await Author.objects.aget(pk=1)
titles = [b.title async for b in Book.objects.all()[:3]]
n = await Book.objects.all()[:3].acount()
One trap does not fit the rule: in Django 6.1.1, aiterator() on a multi-field values_list() raised the error even though it is the async API, because that iterable executes its query when iteration starts rather than lazily; values() and plain async for over the same values_list worked. Building queries is always safe — filter(), order_by(), select_related(), and even str(qs.query) ran without error because none of them touch the database. The rule of thumb: if a line would run SQL in sync code, it needs an a-method or async for in async code. Django's guard checks whether an event loop is running in the current thread; it raises before connecting, so the error points at the exact line.
Verify: grep async views for .get(, .count(), .exists(), .first() and for ... in over querysets.
2. Load relations before you touch them¶
The hidden case is lazy loading. Accessing a foreign key that was not fetched runs a query at attribute access time, and in async code that query is a blocking call:
book = await Book.objects.afirst()
book.author.name # SynchronousOnlyOperation: lazy query for the author
book = await Book.objects.select_related("author").afirst()
book.author.name # "Ursula": the author came with the book
author = await Author.objects.prefetch_related("books").afirst()
len(author.books.all()) # 20100: served from the prefetch cache, no query
Measured: the lazy access raised; select_related and prefetch_related versions worked with no further queries. This is the same discipline that prevents N+1 queries in sync code, now enforced: async code cannot fall back to lazy loading, so every relation a view needs must be loaded up front. Templates rendered from async views follow the same rule — a template that touches book.author will raise if the view did not select_related it. For relations needed only conditionally, await aprefetch_related_objects(instances, "books") loads them on demand.
Verify: every async view's queries include select_related/prefetch_related for each relation its serializer or template reads.
3. Wrap sync helpers instead of calling them¶
Projects that grew up with sync views have helper functions — permission checks, report builders, service-layer functions — that use the sync ORM. Calling one from async code raises; wrap the call:
from asgiref.sync import sync_to_async
def author_stats(author_id: int) -> dict: # existing sync helper
qs = Book.objects.filter(author_id=author_id)
return {"count": qs.count(), "avg_price": qs.aggregate(avg=Avg("price"))["avg"]}
async def author_view(request, pk: int):
stats = await sync_to_async(author_stats)(pk) # whole helper, one thread hop
return JsonResponse(stats)
Wrapping the whole helper, rather than converting each query inside it to an a-method, keeps its queries on one thread and connection and costs one thread hop instead of several. It is also the only correct option when the helper needs a transaction, because Django 6.1 has no async atomic. Keep the default thread_sensitive=True for anything that touches the database: it routes the call to the thread that owns the request's connection.
Verify: sync helpers called from async views appear only inside sync_to_async(...), never bare.
4. Never silence the guard in a server¶
DJANGO_ALLOW_ASYNC_UNSAFE disables the check. Its intended use is environments such as Jupyter notebooks, where an event loop is running but nothing else depends on it. In a server it removes the error by doing exactly what the error warned about:
# With DJANGO_ALLOW_ASYNC_UNSAFE=true, inside an async view:
with connection.cursor() as c:
c.execute("SELECT pg_sleep(0.2)") # now runs on the event loop thread
Measured: five such 200 ms queries from concurrent tasks took 1.06 s and the event loop's lag reached 1,053 ms — every other request on the worker waited a full second. The same five queries through sync_to_async produced a maximum lag of 1 ms. Treat the variable as a notebook-only switch and keep it out of server environments entirely; a stalled loop shows up in measuring event loop lag in production as exactly this kind of spike.
Verify: DJANGO_ALLOW_ASYNC_UNSAFE is not set in any deployed environment's configuration.
5. Mind thread-sensitive calls outside requests¶
Inside a request, Django gives each request its own thread for thread-sensitive work, so concurrent requests' queries run in parallel. Outside a request — a management command, a script, a test, a background coroutine — all thread-sensitive calls share one thread and run one at a time:
from asgiref.sync import ThreadSensitiveContext, sync_to_async
async def one_job(job_id: int) -> None:
async with ThreadSensitiveContext(): # its own thread and connection
await sync_to_async(process_job)(job_id)
async def main(job_ids: list[int]) -> None:
await asyncio.gather(*(one_job(j) for j in job_ids))
Measured with five 200 ms queries in a script: plain sync_to_async calls took 1.07 s — serialized on the shared thread — while the same calls each inside their own ThreadSensitiveContext took 0.25 s. Each context gets its own thread and therefore its own database connection, so close connections at the end of each job (or use the pool) to avoid the per-thread connection buildup described in using the async Django ORM.
Verify: concurrent database work in commands and scripts finishes in about the time of the slowest job, not the sum.
Verification¶
SynchronousOnlyOperation is handled correctly when:
- Async views use
a-methods andasync forfor every query. - Relations are loaded up front with
select_relatedorprefetch_related. - Sync helpers and transactions run whole inside
sync_to_async. DJANGO_ALLOW_ASYNC_UNSAFEappears nowhere in server configuration.
Diagnostic Hook: log SynchronousOnlyOperation occurrences with the view name. In a codebase migrating to async views they cluster by pattern — lazy relations in serializers, helpers shared with sync views — and the counts show which fix to apply widely. A sudden drop to zero with no code changes is a warning sign that someone set the unsafe flag.
Pitfalls & edge cases¶
- Lazy foreign keys. Measured: raised on attribute access; load relations up front.
DJANGO_ALLOW_ASYNC_UNSAFEin servers. Measured: 1,053 ms of event-loop stall.- Converting helpers query by query. Wrap the whole helper in one
sync_to_asynccall. - Concurrency in scripts. Shared-context
sync_to_asynccalls serialize; useThreadSensitiveContext. aiterator()on a multi-fieldvalues_list. Measured: raises in Django 6.1.1; usevalues().
Frequently Asked Questions¶
What does 'You cannot call this from an async context' mean in Django?
Django detected a blocking database call on a thread that is running an event loop and refused to run it. Use the async ORM methods (aget, acount, async for), load relations with select_related, or wrap sync code in sync_to_async.
Why does accessing a foreign key raise SynchronousOnlyOperation?
The related object was not loaded, so attribute access runs a query, which is a blocking call in async code. Use select_related or prefetch_related in the query that fetched the object.
Is it safe to set DJANGO_ALLOW_ASYNC_UNSAFE?
Only where nothing else depends on the event loop, such as a notebook. In a server it runs queries on the loop: five 200 ms queries blocked it for 1,053 ms in testing.
Why are my sync_to_async database calls slow in a management command?
Outside a request, thread-sensitive calls share one thread and run one at a time: five 200 ms queries took 1.07 s. Give each job its own ThreadSensitiveContext and they overlapped in 0.25 s.
Related¶
- Django Async — up to the topic overview.
- Running Django under ASGI with uvicorn — the server these views run on.
- Network I/O & Protocol Handling — the section overview.