Skip to main content

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.

Learning Focus

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:

app/main.py
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 callEffectFix
time.sleep(1) in async defBlocks all requests for 1sawait asyncio.sleep(1)
requests.get(url)Blocks all requests during HTTP callawait httpx.AsyncClient().get(url)
Sync SQLAlchemy session in async defEvery query blocks event loopUse AsyncSession with asyncpg
open(file).read() in async defBlocks during file I/Oawait asyncio.to_thread(open(file).read) or aiofiles
CPU-heavy computationBlocks during computationawait 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

scripts/profile_loop.py
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 / SymptomRoot CauseFix
All requests slow under loadBlocking I/O in async defFind and replace with async equivalent
MissingGreenletSync SQLAlchemy in async contextUse AsyncSession and asyncpg
DetachedInstanceErrorAttribute accessed after session closeexpire_on_commit=False or access within session
RuntimeError: cannot be awaitedUsing sync requests in async defReplace with httpx.AsyncClient
CPU spike, no I/OCPU-bound computation in event loopasyncio.to_thread() or ProcessPoolExecutor

What's Next