Migrating Synchronous Views to Async in Django

python backend django async

Django 3.0 brought ASGI support. Django 3.1 will bring async views. Here’s how to prepare and what performance benefits to expect.

The State of Async in Django 3.0

What works:

What’s coming (3.1):

What’s still sync:

Writing Your First Async View

With Django 3.1+:

import asyncio
from django.http import JsonResponse

async def async_view(request):
    await asyncio.sleep(1)  # Non-blocking sleep
    return JsonResponse({'status': 'ok'})

For now, we can simulate with sync_to_async:

from asgiref.sync import sync_to_async
from django.http import JsonResponse

@sync_to_async
def get_data_from_db():
    return list(MyModel.objects.all())

async def async_view(request):
    data = await get_data_from_db()
    return JsonResponse({'count': len(data)})

When Async Actually Helps

Good: I/O-Bound External Calls

import httpx

async def fetch_external_data(request):
    async with httpx.AsyncClient() as client:
        # These run concurrently
        results = await asyncio.gather(
            client.get('https://api1.example.com/data'),
            client.get('https://api2.example.com/data'),
            client.get('https://api3.example.com/data'),
        )
    return JsonResponse({'results': [r.json() for r in results]})

Three HTTP calls in parallel instead of sequential.

Bad: CPU-Bound Processing

async def process_data(request):
    # This blocks the event loop!
    result = expensive_computation()  # Bad
    return JsonResponse({'result': result})

Async doesn’t help CPU-bound work. Use multiprocessing instead.

Neutral: Database Queries

The ORM is still synchronous. Wrapping in sync_to_async adds overhead:

# This works but doesn't gain much
@sync_to_async
def get_users():
    return list(User.objects.all())

async def user_list(request):
    users = await get_users()
    return JsonResponse({'users': users})

Wait for async ORM (Django 4.1+) for real database benefits.

Mixing Sync and Async

sync_to_async for Sync Functions

from asgiref.sync import sync_to_async

@sync_to_async
def legacy_function():
    # Old sync code
    return do_sync_work()

async def my_view(request):
    result = await legacy_function()
    return JsonResponse(result)

async_to_sync for Calling Async from Sync

from asgiref.sync import async_to_sync

async def fetch_data():
    return await external_api_call()

def sync_view(request):
    data = async_to_sync(fetch_data)()
    return JsonResponse(data)

ASGI Server Configuration

Uvicorn

uvicorn myproject.asgi:application --workers 4

Daphne

daphne -b 0.0.0.0 -p 8000 myproject.asgi:application

Gunicorn with Uvicorn Workers

gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker -w 4

Performance Considerations

Thread Pool for sync_to_async

sync_to_async runs functions in a thread pool:

# Default thread pool size
import concurrent.futures
# Django uses ThreadPoolExecutor with default workers

# For I/O heavy workloads, you might increase this
# but it has limits

Connection Pooling

# For external HTTP calls, reuse clients
from httpx import AsyncClient

# Create once, reuse
http_client = AsyncClient()

async def my_view(request):
    response = await http_client.get('https://api.example.com')
    return JsonResponse(response.json())

Testing Async Views

from django.test import TestCase, AsyncClient

class AsyncViewTests(TestCase):
    async def test_async_view(self):
        client = AsyncClient()
        response = await client.get('/async-endpoint/')
        self.assertEqual(response.status_code, 200)

Or with pytest-asyncio:

import pytest
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_async_view():
    async with AsyncClient(app=app) as client:
        response = await client.get('/async-endpoint/')
        assert response.status_code == 200

Migration Strategy

  1. Start with ASGI: Switch from WSGI to ASGI server
  2. Keep views sync: Everything still works
  3. Identify candidates: Views making external HTTP calls
  4. Convert gradually: One view at a time
  5. Measure: Benchmark before and after

What Not to Do

# Don't use sync libraries in async views
async def bad_view(request):
    import requests
    response = requests.get('https://api.example.com')  # Blocks!
    return JsonResponse(response.json())

# Use async libraries
async def good_view(request):
    import httpx
    async with httpx.AsyncClient() as client:
        response = await client.get('https://api.example.com')
    return JsonResponse(response.json())

Current Limitations

Should You Migrate Now?

Yes, if:

Wait if:

Final Thoughts

Async Django is a journey, not a destination. Django 3.1 brings async views. Future versions will bring async ORM.

Start small. Identify views that make external calls. Convert those first. The full async Django will arrive eventually.


Async is coming. Prepare gradually.

All posts