Asynchronous Support in Django: A Complete Guide to Async Views, ORM, Middleware, Performance, and Safety

This article explains Django’s asynchronous (async) capabilities, including async views, ASGI support, middleware behavior, async ORM features, performance considerations, handling disconnects, and Django’s async safety protections. It also covers how to use sync_to_async(), async ORM methods, and how to safely run synchronous code in async environments.

Django async, ASGIasync views, sync_to_async, async ORM, asyncioSynchronousOnlyOperation, DJANGO_ALLOW_ASYNC_UNSAFE

~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 for works on all QuerySets
  • Async model methods like asave() and aset() 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