Introduction
Modern web applications are often I/O-bound—waiting on databases, APIs, or file systems. Python’s async/await
syntax allows us to handle these operations concurrently without blocking, which can lead to major performance gains. But in FastAPI, using async
carelessly can have the opposite effect.
Let’s explore how a single keyword—async
—can either unlock FastAPI’s full performance potential or become a surprising bottleneck.
Python’s async/await
Python’s async
and await
syntax lets you define coroutines, which can pause execution without blocking the entire program. This is especially useful when doing I/O-bound work, such as waiting for a network response or file read/write.
With async/await
, while one task is waiting, another can run. This allows more efficient use of resources, without spawning threads or processes.
FastAPI Overview
FastAPI is a modern, high-performance Python web framework used for building APIs. It supports both synchronous and asynchronous routes and is designed with performance and developer experience in mind.
FastAPI is asynchronous-first, but that doesn’t mean every route should be async
by default—especially when calling blocking code.
Benchmark Setup
- Machine: MacBook Pro M1
- Python version: 3.11
- FastAPI settings: Default
- Benchmark tool: ApacheBench (ab)
- Test scenario: 100 concurrent users making 200 total requests
- Benchmark Results:
- Requests per second: The server throughput, which is
200 / Total time
. - Total time: Total time to finish the above 200 requests.
- Requests per second: The server throughput, which is
Synchronous vs. Asynchronous Routes
Example Routes
from fastapi import FastAPI
app = FastAPI()
# Synchronous route
@app.get("/sync")
def read_sync():
return {"message": "Hello World"}
# Asynchronous route
@app.get("/async")
async def read_async():
return {"message": "Hello World"}
At first glance, using async def
seems like the right choice—especially for performance. But let’s test that assumption with an I/O-heavy example.
Simulating I/O Operations
# Synchronous I/O operation
import time
def sync_io_heavy():
time.sleep(3)
return {"message": "Hello World"}
Case 1: Synchronous Route with Blocking I/O
@app.get("/")
def read_root():
sync_io_heavy()
sync_io_heavy()
return {"message": "Hello World"}
Benchmark Results:
- Requests per second: 5.53
- Total time: 36 seconds
Case 2: Asynchronous Route with Blocking I/O
@app.get("/")
async def read_root():
sync_io_heavy()
sync_io_heavy()
return {"message": "Hello World"}
Benchmark Results:
- Requests per second: 0.17
- Total time: 1200 seconds
⚠️ This is nearly 40x slower than the synchronous version.
Why?
Even though the route is marked async
, the sync_io_heavy()
function blocks the event loop. In async mode, one blocking call stalls the entire server, preventing it from handling other requests.
Fix: Use Asynchronous I/O
import asyncio
async def async_io_heavy():
await asyncio.sleep(3)
return {"message": "Hello World"}
@app.get("/")
async def read_root():
await async_io_heavy()
await async_io_heavy()
return {"message": "Hello World"}
Benchmark Results:
- Requests per second: 11.06
- Total time: 18 seconds
✅ Nearly 2x faster than the synchronous route!
When You Can’t Make Everything Async
Sometimes, you’re stuck with blocking code (e.g., legacy libraries or synchronous DB drivers). In such cases, you can still use async
routes without hurting performance—by isolating the blocking call in a threadpool.
from fastapi.concurrency import run_in_threadpool
@app.get("/")
async def read_root():
await run_in_threadpool(sync_io_heavy)
await async_io_heavy()
return {"message": "Hello World"}
Benchmark Results:
- Requests per second: 8.29
- Total time: 24 seconds
✅ About 50% faster than using a fully synchronous route.
Conclusion
In FastAPI, async
is powerful—but not magic. Simply slapping async def
on a route doesn’t guarantee better performance, especially if it calls blocking code.
Key Takeaways:
- ✅ Use
async
only when all I/O within the route is non-blocking. - ❌ Avoid using blocking I/O (like
time.sleep()
or standard DB drivers) insideasync def
routes. - ⚙️ Use
run_in_threadpool()
to safely call blocking code in an async context. - 🚀 Proper use of async can double or even triple your performance.
A single async
keyword can make or break your FastAPI application’s throughput—use it wisely.
Reference
- FastAPI Documentation: https://fastapi.tiangolo.com/async/