Migrating Synchronous Views to Async in Django
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:
- ASGI application support
- Running under Uvicorn/Daphne
- Async middleware
What’s coming (3.1):
- Async views
- Async handlers
What’s still sync:
- ORM queries
- Template rendering
- Most Django internals
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
- Start with ASGI: Switch from WSGI to ASGI server
- Keep views sync: Everything still works
- Identify candidates: Views making external HTTP calls
- Convert gradually: One view at a time
- 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
- ORM isn’t async (coming in Django 4.1+)
- Template rendering is sync
- Many third-party packages are sync-only
- Async adds complexity
Should You Migrate Now?
Yes, if:
- You make many external HTTP calls per request
- You’re building WebSocket features (with Channels)
- You have specific async requirements
Wait if:
- Your bottleneck is database queries (ORM still sync)
- Your team isn’t familiar with async Python
- You’re on Django < 3.1
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.