~6 min read • Updated Mar 15, 2026
Introduction
Django provides first-class support for asynchronous (“async”) views and an entirely async-enabled request stack when running under ASGI. Async views also work under WSGI, but with performance limitations and without the benefits of long-lived, concurrent connections.
While Django’s async support is still evolving—especially in the ORM—developers can already build powerful async-enabled applications using sync_to_async() and async-native Python libraries.
Async Views
Any Django view can be asynchronous by defining it with async def.
For function-based views, simply declare the view as async.
For class-based views, declare async versions of HTTP method handlers like get() or post().
async def my_view(request):
return HttpResponse("Hello async world!")
Django uses asgiref.sync.iscoroutinefunction to detect async views.
If you implement custom coroutine logic, use markcoroutinefunction to ensure Django recognizes it.
Async Views Under WSGI
Async views run inside a one-off event loop. You can use async features, but you won’t get the scalability benefits of ASGI.
Async Views Under ASGI
This is where async shines—Django can handle hundreds of concurrent connections without threads, enabling:
- long-polling
- streaming responses
- slow or incremental data delivery
To use these features, deploy Django with ASGI.
Middleware and Async Behavior
You only get a fully async request stack if all middleware is async-capable. If even one middleware is synchronous, Django must switch to a thread-per-request model.
To debug this, enable django.request debug logging and look for messages like:
"Asynchronous handler adapted for middleware ..."
Even with mixed sync/async stacks, you can still run async code concurrently—useful for external API calls.
Calling Sync Django Code from Async
Many Django components—including parts of the ORM—are still synchronous.
To call them safely from async views, wrap them with sync_to_async():
from asgiref.sync import sync_to_async
result = await sync_to_async(sync_function, thread_sensitive=True)(pk=123)
Calling synchronous Django code directly inside async views triggers Django’s async safety protection.
Decorators Compatible with Async Views
Most Django decorators work with both sync and async views, including:
- cache_control()
- never_cache()
- csrf_exempt()
- csrf_protect()
- require_GET()
- require_POST()
- etag()
- last_modified()
- vary_on_headers()
- xframe_options_deny()
- ...and many more
Example:
@never_cache
async def my_async_view(request):
...
Async ORM Support
Django supports asynchronous ORM operations with a-prefixed methods:
async for author in Author.objects.filter(name__startswith="A"):
book = await author.books.afirst()
Key Features
- All query-triggering methods have async variants (e.g.,
acreate(),aget()) async forworks on all QuerySets- Async model methods like
asave()andaset()are supported
Limitations
- Transactions do not work in async mode
- Persistent DB connections (
CONN_MAX_AGE) should be disabled - Use external connection pooling if needed
Performance Considerations
When Django must switch between sync and async modes, it incurs a small overhead (~1 ms). This happens when:
- Async views run under WSGI
- Sync middleware wraps async views
- Async middleware wraps sync views
If your project is fully synchronous, ASGI may still improve performance due to async request handling.
Handling Client Disconnects
Long-running async views may encounter client disconnects.
Django raises asyncio.CancelledError:
async def my_view(request):
try:
...
except asyncio.CancelledError:
# cleanup
raise
Async Safety
Some Django components are async-unsafe due to global state. Calling them inside an async context raises SynchronousOnlyOperation.
Fixing Async Safety Errors
- Move sync code into a synchronous function
- Call it using
sync_to_async()
Environments That Impose Async Context
Tools like Jupyter and IPython run an event loop automatically. Disable it with:
%autoawait off
Re-enable it with:
%autoawait on
DJANGO_ALLOW_ASYNC_UNSAFE
If absolutely necessary, you can disable async safety checks:
import os
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
Warning: This can cause data corruption if used with concurrent access. Never use it in production.
Conclusion
Django’s asynchronous support opens the door to highly scalable, concurrent applications—especially when deployed under ASGI.
With async views, async ORM methods, and careful use of sync_to_async(), developers can build modern, efficient systems while maintaining Django’s familiar structure.
As Django continues to expand its async capabilities, the ecosystem will only grow more powerful.
Introduction
When working with Django’s asynchronous features, you often need to call synchronous code from an async context—or call async code from a synchronous context.
To make this possible, Django relies on two adapter functions from asgiref.sync:
- async_to_sync()
- sync_to_async()
These functions ensure compatibility between Django’s traditional synchronous architecture and its growing async ecosystem.
async_to_sync()
The async_to_sync function takes an async function and returns a synchronous wrapper around it.
Example Usage
from asgiref.sync import async_to_sync
async def get_data():
...
sync_get_data = async_to_sync(get_data)
@async_to_sync
async def get_other_data():
...
How It Works
- If an event loop already exists in the current thread, the async function runs inside it.
- If no event loop exists, a temporary one is created and destroyed after execution.
- The async function always runs in a separate thread from the caller.
- Threadlocals and contextvars are preserved across the boundary.
In essence, async_to_sync is a more advanced version of Python’s asyncio.run(), with additional support for thread-sensitive operations.
sync_to_async()
The sync_to_async function takes a synchronous function and returns an async wrapper around it.
Example Usage
from asgiref.sync import sync_to_async
async_function = sync_to_async(sync_function, thread_sensitive=False)
@sync_to_async
def sync_function():
...
Threadlocal and Context Preservation
Just like async_to_sync, this adapter preserves threadlocal and contextvars values across the sync/async boundary.
Threading Modes
- thread_sensitive=True (default): The sync function runs in the same thread as other thread-sensitive functions—usually the main thread.
- thread_sensitive=False: The sync function runs in a brand-new thread that is destroyed afterward.
Note:
Since asgiref 3.3.0, the default value of thread_sensitive is True.
Why Thread Sensitivity Matters
Many libraries—especially database drivers—require that connections be used in the same thread where they were created. Much of Django’s synchronous code also assumes a single-threaded execution model.
Thread-sensitive mode ensures that all synchronous Django code runs safely in one thread, even when called from async views.
Important Warning: Do Not Pass Database Connections
You must not pass thread-sensitive objects like database connections into sync_to_async().
Doing so triggers thread-safety errors:
- SynchronousOnlyOperation when used directly in async context
- DatabaseError when used across threads
Incorrect Usage
await sync_to_async(connection.cursor)()
Correct Usage
Wrap all database access inside a synchronous helper function:
def fetch_data():
with connection.cursor() as cursor:
cursor.execute("SELECT ...")
return cursor.fetchall()
result = await sync_to_async(fetch_data)()
Conclusion
The async_to_sync and sync_to_async adapters are essential tools for bridging Django’s synchronous core with modern asynchronous workflows. They allow developers to safely mix sync and async code, interact with the ORM, and build scalable ASGI applications without breaking thread safety or event loop rules.
Understanding these adapters is key to writing robust hybrid Django applications that take advantage of both synchronous and asynchronous capabilities.
Written & researched by Dr. Shahin Siami