0

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>.
  • rewrk for load testing.
    • Command: rewrk -d 30s -c 64 --host <host with route>.
  • 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

  1. 📦 Static Content: Raw server performance without DB.
  2. 📖 DB Read: A simple query to read data (simple join).
  3. 🔥 Contentious Write: Using SELECT FOR UPDATE to simulate high data contention.

📊 Test Results

We compare four configurations:

  1. Sync Django: Standard WSGI.
  2. Sync Django Pooled: WSGI with DB Connection Pooling (often forgotten, but critical).
  3. Async Django: ASGI mode.
  4. FastAPI: Native Async with SQLAlchemy.

1. Static Content (No Database)

This is the pure speed of the framework.

ServerWorkersRPS (Req/s)Avg Latency
Sync Django15,03213ms
Sync Django26,61410ms
Async Django1551116ms
Async Django21,12057ms
FastAPI126,2872.43ms
FastAPI237,3531.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.

ServerConfigRPS (Req/s)Avg Latency
Sync Django1 worker456140ms
Sync Django Pooled2 workers1,82235ms
Async Django2 workers541118ms
FastAPI2 workers409156ms

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

ServerConfigRPSAvg Latency
Sync Django1 worker170376ms
Sync Django Pooled2 workers160401ms
Async Django2 workers169378ms

⚖️ 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:

  1. Sync Django Pooled is often the best option (performance + simplicity).
  2. FastAPI is only faster if it is the only bottleneck (e.g., proxy, calculations without DB).
  3. 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! 📉➡️📈

Comments