What Does Async Really Mean for Your Python Web App?
In Brief ⏱️: Async is not a silver bullet. If, like 80% of applications, your bottleneck is the database, switching to async without a rigorous connection pooling strategy risks degrading your performance instead of improving it.
The Python community is buzzing with improved async support. If you have an existing service, you might be wondering if you’re missing out on the “hype.”
🚀 Benchmarks often show higher throughput and promise to handle more requests with fewer resources. But will switching to async really be a “free lunch” for your service?
The reality often differs from expectations. Unless you are working in a highly distributed environment where your service is the only bottleneck (requiring 10 instances just to keep up with traffic), you probably won’t see the promised gains.
⚠️ Warning: You might even see a performance drop by migrating blindly.
🧪 Benchmarking Method
The gap between theory and practice often comes from application design. Standard services talk directly to a database. As traffic increases, the load on the database grows faster than on the Python code.
The bottleneck thus shifts from the code to the DB. For these tests, we simulate this reality with:
- Django + PostgreSQL (Classic and robust stack).
- FastAPI (The “async-first” challenger).
🛠️ See technical configuration details (Click to expand)
To run the benchmarks, I use the following configuration:
- Granian as server (supports threads and processes).
- Command:
granian --interface <asgi or wsgi> --workers <1 or 2> --blocking-threads 32 --port 3000 <application>.
- Command:
- rewrk for load testing.
- Command:
rewrk -d 30s -c 64 --host <host with route>.
- Command:
- Hardware (System76 Darter Pro):
- NixOS unstable
- 12th Gen Intel® Core™ i7-1260P × 16
- 64 GiB RAM
- Python 3.14
- PostgreSQL 18
Note: “Free-threaded” benchmarks will arrive in a future article once psycopg support stabilizes.
The 3 Scenarios Tested
- 📦 Static Content: Raw server performance without DB.
- 📖 DB Read: A simple query to read data (simple join).
- 🔥 Contentious Write: Using
SELECT FOR UPDATEto simulate high data contention.
📊 Test Results
We compare four configurations:
- Sync Django: Standard WSGI.
- Sync Django Pooled: WSGI with DB Connection Pooling (often forgotten, but critical).
- Async Django: ASGI mode.
- FastAPI: Native Async with SQLAlchemy.
1. Static Content (No Database)
This is the pure speed of the framework.
| Server | Workers | RPS (Req/s) | Avg Latency |
|---|---|---|---|
| Sync Django | 1 | 5,032 | 13ms |
| Sync Django | 2 | 6,614 | 10ms |
| Async Django | 1 | 551 | 116ms |
| Async Django | 2 | 1,120 | 57ms |
| FastAPI | 1 | 26,287 | 2.43ms |
| FastAPI | 2 | 37,353 | 1.71ms |
Observations:
- 🏆 FastAPI dominates totally on static content. It is optimized for this.
- 📉 Async Django is surprisingly slower than its Sync version here.
- However, Async scales better: doubling workers almost doubles throughput.
2. Database Reads (The Real World)
Let’s connect a PostgreSQL DB. We fetch a quote and its author.
Note: Here, we are mostly testing the database. Differences come from the framework overhead.
| Server | Config | RPS (Req/s) | Avg Latency |
|---|---|---|---|
| Sync Django | 1 worker | 456 | 140ms |
| Sync Django Pooled | 2 workers | 1,822 | 35ms |
| Async Django | 2 workers | 541 | 118ms |
| FastAPI | 2 workers | 409 | 156ms |
💥 The Shock:
- Sync Django with Pooling crushes the competition (+200% performance!).
- The async overhead is felt negatively.
- Even FastAPI lags behind once the DB is involved without optimized pooling (or equivalent).
3. Contentious Writes (The Nightmare)
We simulate concurrent transactions that must lock rows (view counter). Everyone is fighting for the same resource.
| Server | Config | RPS | Avg Latency |
|---|---|---|---|
| Sync Django | 1 worker | 170 | 376ms |
| Sync Django Pooled | 2 workers | 160 | 401ms |
| Async Django | 2 workers | 169 | 378ms |
⚖️ Perfect Tie. Here, it is the database that blocks everyone. Whether your code is async or sync, everyone waits for the DB lock. However, DB pooling helps smooth out extreme latency peaks.
Conclusion
These benchmarks reveal an important truth: Django and Python are hyper-optimized for synchronous mode.
✅ Key Takeaways:
- Sync Django Pooled is often the best option (performance + simplicity).
- FastAPI is only faster if it is the only bottleneck (e.g., proxy, calculations without DB).
- Async has a non-negligible overhead.
Friendly Advice: Don’t change your architecture to follow a trend. If you are doing classic CRUD, stay Synchronous and enable DB Pooling! 📉➡️📈