Async and Blocking Issues
Async bugs are the hardest to diagnose in FastAPI because they manifest as intermittent slowdowns, not immediate errors. A single blocking call in an async route can stall all concurrent requests.
By the end of this lesson you can: identify blocking calls in async routes, use asyncio.to_thread for sync I/O, diagnose MissingGreenlet errors, and profile event loop blocking.
Detecting Blocking in Async Routes
Symptom: Under load, all requests slow down simultaneously. A single slow request causes all others to queue.
Diagnose with aiodebug:
import asyncio
# Enable slow coroutine logging (development only)
asyncio.get_event_loop().set_debug(True)
asyncio.get_event_loop().slow_callback_duration = 0.1 # Warn if >100ms
This logs a warning whenever a coroutine holds the event loop for more than 100ms.
Common Blocking Mistakes
| Blocking call | Effect | Fix |
|---|---|---|
time.sleep(1) in async def | Blocks all requests for 1s | await asyncio.sleep(1) |
requests.get(url) | Blocks all requests during HTTP call | await httpx.AsyncClient().get(url) |
Sync SQLAlchemy session in async def | Every query blocks event loop | Use AsyncSession with asyncpg |
open(file).read() in async def | Blocks during file I/O | await asyncio.to_thread(open(file).read) or aiofiles |
| CPU-heavy computation | Blocks during computation | await asyncio.to_thread(compute) or ProcessPoolExecutor |
MissingGreenlet Error
Full error: sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_() here
Cause: Calling async SQLAlchemy in a non-async context, or using the sync engine when you have an async one.
# ❌ Wrong: sync engine in async route
from sqlalchemy import create_engine
engine = create_engine("postgresql://...") # Sync engine
@app.get("/users")
async def get_users():
with engine.connect() as conn: # MissingGreenlet!
...
# ✅ Correct: async engine
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine("postgresql+asyncpg://...")
@app.get("/users")
async def get_users():
async with engine.connect() as conn:
...
Lazy Loading in Async Context
Full error: sqlalchemy.exc.MissingGreenlet when accessing a relationship attribute.
Cause: SQLAlchemy lazy-loads relationships synchronously — this is not possible in async.
# ❌ Lazy load triggers sync DB call in async context
user = await db.get(User, user_id)
posts = user.posts # MissingGreenlet!
# ✅ Eager load with selectinload
from sqlalchemy.orm import selectinload
result = await db.execute(
select(User).where(User.id == user_id).options(selectinload(User.posts))
)
user = result.scalar_one_or_none()
posts = user.posts # Already loaded — no extra query
DetachedInstanceError
Cause: Accessing an ORM attribute after the session is closed.
# ❌ Session closes after get_db yields, user object detaches
async def get_user(db):
user = await db.get(User, 1)
return user
@app.get("/")
async def route(db: DBSession):
user = await get_user(db)
return {"name": user.username} # DetachedInstanceError if expire_on_commit=True
Fix: Set expire_on_commit=False in async_sessionmaker.
Profiling Event Loop Blocking
import asyncio
import time
async def blocking_route_simulation():
time.sleep(2) # This blocks the entire event loop
async def main():
start = asyncio.get_event_loop().time()
await asyncio.gather(
blocking_route_simulation(),
asyncio.sleep(0.1), # This will be delayed
)
print(f"Total time: {asyncio.get_event_loop().time() - start:.2f}s")
# Expected: 0.1s, Actual: 2s — event loop blocked!
asyncio.run(main())
Common Pitfalls Summary
| Error / Symptom | Root Cause | Fix |
|---|---|---|
| All requests slow under load | Blocking I/O in async def | Find and replace with async equivalent |
MissingGreenlet | Sync SQLAlchemy in async context | Use AsyncSession and asyncpg |
DetachedInstanceError | Attribute accessed after session close | expire_on_commit=False or access within session |
RuntimeError: cannot be awaited | Using sync requests in async def | Replace with httpx.AsyncClient |
| CPU spike, no I/O | CPU-bound computation in event loop | asyncio.to_thread() or ProcessPoolExecutor |