This lesson on Async Endpoints — When to Use Them is hands-on and example-driven. You will evaluate concurrency models in FastAPI and choose between synchronous (def) and asynchronous (async def) endpoints based on I/O characteristics and concurrency demands. You will also configure asynchronous database drivers like asyncpg with SQLAlchemy to prevent event loop blocking in production.
What You'll Be Able To Do
- Configure parallel synchronous and asynchronous SQLAlchemy database sessions and drivers in FastAPI.
- Implement equivalent CRUD operations using synchronous and
async/awaitsyntax. - Analyze execution mechanics between worker thread pools and the single-threaded event loop.
- Identify performance bottlenecks caused by database write serialization and low-latency testing skew.
- Apply an architectural decision framework to choose between
defandasync defbased on workload type.
Detailed Concept Walkthrough
1. Sync vs Async Execution Models
FastAPI executes synchronous def endpoints inside an external thread pool, whereas async def endpoints run directly on the main event loop thread.
- Execution Flow: When a request hits a synchronous
defendpoint, FastAPI offloads execution to an AnyIO thread pool so the main event loop remains unblocked while the thread waits on I/O. - Under the Hood: An
async defendpoint executes directly on the single-threaded event loop; when it encounters anawaitexpression, it yields control back to the loop to process other incoming tasks. - Critical Rule: Calling a blocking operation (such as
time.sleep()or a synchronous database driver) inside anasync deffunction blocks the entire event loop, freezing all concurrent requests.
# Synchronous endpoint (runs in thread pool)
@router.get("/sync-items")
def get_sync_items(db: Session = Depends(get_sync_db)):
return db.query(Item).all()
# Asynchronous endpoint (runs on event loop)
@router.get("/async-items")
async def get_async_items(db: AsyncSession = Depends(get_async_db)):
result = await db.execute(select(Item))
return result.scalars().all()
Key Takeaway: Async uses a cooperative single-threaded event loop via
await, while sync relies on separate threads per blocking request.
2. Database Drivers and Connection Strings
True asynchronous database operations require specialized non-blocking client drivers; using a standard driver inside an async function negates all async benefits.
- Mechanism: Standard drivers like
psycopg2use blocking network sockets that freeze execution during I/O transactions. - Under the Hood: Asynchronous drivers like
asyncpgimplement non-blocking socket communication using Python's asyncio protocol, enabling cooperative context switches during database wait times. - Syntax Rule: The SQLAlchemy connection URL must explicitly specify the asynchronous driver dialect prefix, such as
postgresql+asyncpg://instead ofpostgresql://.
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
# Sync connection URL: postgresql://user:pass@localhost/dbname
# Async connection URL requires the async driver dialect:
ASYNC_DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/dbname"
async_engine = create_async_engine(ASYNC_DATABASE_URL, echo=False)
AsyncSessionLocal = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
Key Takeaway: Always pair
async defroutes with non-blocking drivers likeasyncpgto prevent stalling the event loop during database I/O.
3. Benchmarking Realities and Testing Skew
Local benchmark tests often favor synchronous endpoints due to minimal network latency and async context-switching overhead.
- Mechanism: In localhost or Docker test environments with near-zero latency, the overhead of creating coroutines and scheduling context switches exceeds the execution time of simple synchronous queries.
- Under the Hood: Relational databases enforce write serialization and strict connection pool constraints, meaning database-level locks bottleneck concurrent writes regardless of application-level concurrency.
- Best Practice: Evaluate async performance under realistic network latency and high concurrent connection counts rather than relying on localhost micro-benchmarks.
# Example benchmark invocation comparing throughput
# wrk -t12 -c400 -d30s http://127.0.0.1:8000/sync-items
# wrk -t12 -c400 -d30s http://127.0.0.1:8000/async-items
# Note: Async shines under high concurrency (>1000 conns) and remote I/O latency.
Key Takeaway: Local low-latency tests obscure async advantages because context switching overhead and database write serialization dominate execution time.
4. Architectural Decision Framework
Select endpoint concurrency models based on whether workloads are I/O-bound, CPU-bound, or constrained by third-party library support.
- Async Criteria: Choose
async deffor microservices with high concurrent connections, frequent outbound HTTP/API calls, WebSockets, or streaming services with native async drivers. - Sync Criteria: Choose standard
deffor CPU-intensive data processing, simple internal CRUD with low concurrency, or applications relying on legacy blocking client libraries. - Best Practice: Avoid mixing sync blocking calls inside
async defroutes; if a blocking library must be used, implement it inside a standarddefroute to leverage FastAPI's thread pool.
# Correct pattern for blocking/CPU operations:
@router.post("/cpu-heavy-process")
def process_data(payload: dict):
# Runs safely in threadpool without blocking event loop
return compute_heavy_hash(payload)
# Correct pattern for outbound non-blocking I/O:
@router.get("/external-call")
async def call_microservice(client: httpx.AsyncClient = Depends(get_http_client)):
response = await client.get("https://api.external.com/data")
return response.json()
Key Takeaway: Use
async deffor high-concurrency non-blocking I/O, and usedeffor CPU tasks or blocking dependencies.
Topics Covered in Async Endpoints — When to Use Them
- Environment Setup (0:33 - 1:18) — FastAPI routers and dual synchronous and asynchronous database sessions are configured for testing.
- CRUD Implementation Comparison (1:18 - 3:04) — Side-by-side CRUD routes contrast standard SQLAlchemy query syntax with async await operations.
- Database Driver Configuration (3:04 - 3:41) — The necessity of using async-compatible connection drivers like asyncpg instead of standard drivers is demonstrated.
- Sync vs Async Architecture (3:41 - 4:31) — The mechanical differences between thread pool offloading and cooperative single-threaded event loops are explained.
- Performance Benchmarking (4:31 - 6:01) — Benchmark results illustrate how low-latency environments and database serialization limits influence throughput metrics.
- Architectural Decision Framework (6:01 - 7:07) — Guidelines establish when to select async endpoints over sync endpoints based on I/O profiles and concurrency requirements.
FastAPI Cheat Sheet
-
async def endpoint()— Defines route running directly on main event loop@app.get("/data") async def get_data(): return {"status": "ok"} -
def endpoint()— Defines route running in AnyIO thread pool@app.get("/data") def get_data(): return {"status": "ok"} -
postgresql+asyncpg://— Database dialect specifying the non-blocking asyncpg driverURL = "postgresql+asyncpg://user:pwd@db:5432/app" -
create_async_engine()— Creates asynchronous SQLAlchemy database engine instanceengine = create_async_engine(URL) -
await db.execute(query)— Executes query asynchronously and yields event loopres = await db.execute(select(User)) users = res.scalars().all() -
await db.commit()— Asynchronously flushes and commits database transactiondb.add(new_item) await db.commit()
Comparison Table
| Dimension | Synchronous (def) | Asynchronous (async def) |
|---|---|---|
| Runtime Mechanism | FastAPI thread pool worker | Single main event loop thread |
| Database Driver | Standard driver (psycopg2) | Async driver (asyncpg) |
| Context Switch Cost | OS thread switching overhead | Lightweight coroutine context switch |
| Blocking Code Impact | Blocks single worker thread | Freezes entire server event loop |
| Ideal Workload | CPU-bound or simple CRUD | High-concurrency remote I/O |
Common Pitfalls
- Mistake: Calling blocking functions like time.sleep or requests.get inside async def endpoints. Avoid: Use asyncio.sleep and httpx.AsyncClient or declare the endpoint with def.
- Mistake: Using standard synchronous database drivers with async SQLAlchemy sessions. Avoid: Specify an asynchronous driver dialect such as postgresql+asyncpg in the database URL.
- Mistake: Expecting async to accelerate local write operations on single-node databases. Avoid: Account for database serialization limits and benchmark with realistic network latency.
FAQs
- Why did my synchronous endpoint benchmark faster than my async endpoint on localhost? Near-zero localhost latency makes the overhead of coroutine creation and event loop scheduling larger than the query execution time.
- What happens if I use a blocking database driver inside an async def endpoint? The blocking driver halts the single event loop thread, preventing the server from handling any other concurrent incoming requests.
- Can async endpoints speed up CPU-bound operations?
No, async only benefits I/O-bound wait operations; CPU-intensive calculations will monopolize the event loop and should use standard
def.