This repository demonstrates the performance difference between Django's synchronous WSGI and asynchronous ASGI request handling when dealing with blocking I/O operations (like Celery task calls).
- Docker installed and running
- 2GB free disk space
- Ports 8001 and 8002 available
# Clone the repository
git clone <repository-url>
cd demoasync
# Run the benchmark (takes ~5 minutes)
./run_benchmark.shThat's it! The script will automatically:
- Build all containers
- Start Django servers
- Find performance limits
- Show you the results
The benchmark simulates a common Django pattern: receiving a webhook and enqueueing a Celery task. The fake_celery_task.delay() method blocks for 20ms to simulate the time it takes to serialize and push a task to a message broker.
class fake_celery_task:
def delay():
time.sleep(0.02) # Simulates 20ms blocking Celery call
# Sync view - blocks the entire worker
def ping_sync(request):
fake_celery_task.delay()
return JsonResponse({"mode": "sync"})
# Async view - uses sync_to_async to handle blocking code
async def ping_async(request):
await sync_to_async(fake_celery_task.delay, thread_sensitive=False)()
return JsonResponse({"mode": "async"})Important: Even though we use async views, Celery itself is still synchronous and blocking. The sync_to_async wrapper runs the blocking code in a thread pool, allowing the async event loop to handle other requests meanwhile. For fully async Celery operations, you'd need something like aio-celery.
Open the HTML plots in ./results/ to see latency distributions:
sync_300rps.html- Sync at 300 req/sasync_5000rps.html- Async at 5000 req/s
The benchmark will test increasing request rates until it finds the limit:
Testing sync mode...
====================
Finding maximum sustainable throughput (>99% success rate)...
Testing 50 req/s: โ
Success: 100.00% Latency: 22.34ms Actual: 49.99 req/s
Testing 100 req/s: โ
Success: 100.00% Latency: 22.89ms Actual: 99.97 req/s
Testing 200 req/s: โ
Success: 100.00% Latency: 23.45ms Actual: 199.94 req/s
Testing 300 req/s: โ
Success: 100.00% Latency: 24.12ms Actual: 299.87 req/s
Testing 400 req/s: โ Success: 89.23% Latency: 234.56ms Actual: 356.92 req/s
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ฏ PERFORMANCE LIMIT FOUND!
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Maximum sustainable rate for sync mode:
๐ Throughput: 299.87 req/s
โฑ๏ธ Latency: 24.12ms
โ
Success: >99%
System starts failing at: 400 req/s
On a typical machine with 8 workers:
- Sync mode: ~300 requests/second
- Async mode: ~5,000 requests/second
- Improvement: Async is 16x faster
Sync mode: Each worker can only handle one request at a time. With 20ms blocking per request, each worker maxes out at 50 req/s. With 8 workers: 8 ร 50 = 400 req/s theoretical max.
Async mode: Workers can handle multiple requests concurrently. While one request is blocked on I/O, the worker can process others. This allows much higher throughput with the same number of workers.
django_project/myproject/views.py- The sync and async endpointsvegeta-tester/find_limits.sh- Load testing script that finds performance limitsdocker-compose.yml- Container configurationresults/- Generated performance reports and charts
The benchmark finds the maximum request rate where the server maintains >99% success rate. Results include:
- Throughput: Requests per second the server can handle
- Latency: Response time at that throughput
- HTML plots: Visual latency distribution charts
If your Django app:
- Receives webhooks that trigger Celery tasks
- Makes API calls to external services
- Queries slow databases
- Does any blocking I/O
Then async Django can handle significantly more traffic with the same hardware.
The benchmark uses:
- uv: Fast Python package installer (10-100x faster than pip)
- Gunicorn: Production WSGI/ASGI server
- Uvicorn: ASGI worker for async mode
- Vegeta: HTTP load testing tool
The two Django instances use different Gunicorn configurations:
# Sync server - uses WSGI interface (synchronous only)
gunicorn myproject.wsgi:application \
--workers 8 \
--bind 0.0.0.0:8000
# Async server - uses ASGI interface (async-capable)
gunicorn myproject.asgi:application \
--workers 8 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000The key differences:
wsgi:applicationloads Django's WSGI application (sync-only)asgi:applicationloads Django's ASGI application (async-capable)--worker-class uvicorn.workers.UvicornWorkeris required for ASGI
The critical difference is --worker-class uvicorn.workers.UvicornWorker which enables async request handling. Check docker-compose.yml for the exact commands.
- This tests a specific pattern (webhook โ Celery). Your results may vary.
- Async benefits decrease if your code is CPU-bound rather than I/O-bound.
- Real Celery calls may take more or less than 20ms.
- Database queries in async mode need async-compatible drivers.
# Stop all containers
docker-compose down
# Remove all containers and images
docker-compose down --rmi all