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

Related Articles

Django Tasks Framework: A Complete Guide to Background Task Execution in Django 6.0

Django 6.0 introduces the Tasks framework, a built‑in system for defining and queuing background work outside the request–response cycle. This article explains how Tasks work, how to configure backends, how to define and enqueue tasks, how context works, and how to integrate third‑party worker systems for production environments.

Continue

Django System Check Framework: A Complete Guide to Writing, Registering, Running, and Testing System Checks

This article explains Django’s System Check Framework—a powerful mechanism for detecting configuration issues, validating project structure, and ensuring code quality. It covers how checks are executed, how to write custom checks, how messages work, how to register and tag checks, how to extend checks for fields and models, and how to write both unit and integration tests for system checks.

Continue

Django Signals: A Complete Guide to Listening, Connecting, Sending, and Managing Application Events

This article provides a comprehensive explanation of Django’s signal system—an event‑driven mechanism that allows decoupled applications to react to actions occurring elsewhere in the framework. It covers how to define receivers, connect signals, use decorators, handle specific senders, organize signal code, and follow best practices to avoid complexity.

Continue

Understanding Django Settings: Configuration, Environment Management, and Best Practices

This article provides a complete overview of Django’s settings system. It explains how settings files work, how to designate a settings module, how to use settings in your code, how to configure Django manually, how to secure sensitive settings, and how to work with custom default settings. It also covers the role of django.setup() for standalone scripts.

Continue

Serializing and Deserializing Django Objects: A Complete Guide to Django’s Serialization Framework

This article explains Django’s serialization framework, including how to serialize and deserialize model instances, work with subsets of fields, handle inherited models, use different serialization formats (JSON, XML, YAML, JSONL), and understand how relational fields are represented. It also covers DeserializedObject behavior and common pitfalls.

Continue

Performance and Optimization in Django: A Complete Guide to Benchmarking, Caching, Laziness, and Efficient Code Design

This article provides a comprehensive overview of performance and optimization techniques in Django. It covers benchmarking, profiling tools, choosing the right abstraction level, caching strategies, understanding laziness, and how Django’s QuerySets benefit from delayed evaluation.

Continue