enigma/demoasync

โ˜… 0Forks 0ShellGitHub โ†—Compare

README

Django Sync vs Async Performance Benchmark

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).

๐Ÿš€ Quick Start

Prerequisites

  • Docker installed and running
  • 2GB free disk space
  • Ports 8001 and 8002 available

Run the Benchmark

# Clone the repository
git clone <repository-url>
cd demoasync

# Run the benchmark (takes ~5 minutes)
./run_benchmark.sh

That's it! The script will automatically:

  1. Build all containers
  2. Start Django servers
  3. Find performance limits
  4. Show you the results

What This Demonstrates

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.

Key Difference #1: Async Views with Blocking Celery

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.

View Detailed Results

Open the HTML plots in ./results/ to see latency distributions:

  • sync_300rps.html - Sync at 300 req/s
  • async_5000rps.html - Async at 5000 req/s

Understanding the Output

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

Expected Results

On a typical machine with 8 workers:

  • Sync mode: ~300 requests/second
  • Async mode: ~5,000 requests/second
  • Improvement: Async is 16x faster

Why Such a Difference?

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.

Key Files

  • django_project/myproject/views.py - The sync and async endpoints
  • vegeta-tester/find_limits.sh - Load testing script that finds performance limits
  • docker-compose.yml - Container configuration
  • results/ - Generated performance reports and charts

Understanding the Results

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

Real-World Implications

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.

Configuration

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

Key Difference #2: How the Servers are Started

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:8000

The key differences:

  • wsgi:application loads Django's WSGI application (sync-only)
  • asgi:application loads Django's ASGI application (async-capable)
  • --worker-class uvicorn.workers.UvicornWorker is 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.

Caveats

  1. This tests a specific pattern (webhook โ†’ Celery). Your results may vary.
  2. Async benefits decrease if your code is CPU-bound rather than I/O-bound.
  3. Real Celery calls may take more or less than 20ms.
  4. Database queries in async mode need async-compatible drivers.

Cleanup

# Stop all containers
docker-compose down

# Remove all containers and images
docker-compose down --rmi all

Further Reading

Contributors

enigma

Issues