Skip to content

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

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.

Eleven ORM patterns run from async code A grid of 7 rows by 3 columns. Eleven ORM patterns run from async code pattern in async code result fix Author.objects.get(pk=1) SynchronousOnlyOperation await aget() for b in Book.objects.all() SynchronousOnlyOperation async for len(queryset) SynchronousOnlyOperation await acount() (await Book.objects.afirst()).author.name SynchronousOnlyOperation select_related for b in author.books.all() SynchronousOnlyOperation async for, or prefetch sync helper that queries, called directly SynchronousOnlyOperation sync_to_async(helper) select_related, prefetch_related, str(qs.query) worked already safe Django 6.1.1; every failure raised before reaching the database.

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.

How should this SynchronousOnlyOperation be fixed? A decision on What triggered it with 4 outcomes. How should this SynchronousOnlyOperation be fixed? What triggered it? a queryset call a-method or async for aget, acount, async for book.author, author.books select_related / prefetch_related load up front a sync helper or transaction sync_to_async(helper) one hop, one connection tempted to set ALLOW_ASYNC_UNSAFE do not, in servers 1,053 ms loop stall The error marks a blocking call; every real fix moves the call off the loop.

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.

Five 200 ms queries from async code 3 horizontal bars comparing ALLOW_ASYNC_UNSAFE: max loop lag with the others. Five 200 ms queries from async code ALLOW_ASYNC_UNSAFE: max loop lag 1,053 ms sync_to_async, shared context: total time 1.07 s (lag 1 ms) own ThreadSensitiveContext each: total time 0.25 s Django 6.1.1, PostgreSQL 17, run from a standalone script. Unsafe mode stalls the loop; a shared context serializes; separate contexts overlap.

Verification

SynchronousOnlyOperation is handled correctly when:

  • Async views use a-methods and async for for every query.
  • Relations are loaded up front with select_related or prefetch_related.
  • Sync helpers and transactions run whole inside sync_to_async.
  • DJANGO_ALLOW_ASYNC_UNSAFE appears 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_UNSAFE in servers. Measured: 1,053 ms of event-loop stall.
  • Converting helpers query by query. Wrap the whole helper in one sync_to_async call.
  • Concurrency in scripts. Shared-context sync_to_async calls serialize; use ThreadSensitiveContext.
  • aiterator() on a multi-field values_list. Measured: raises in Django 6.1.1; use values().

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.