What is the difference between async def and def path operations in FastAPI?
FastAPI handles async def and def path operations differently in terms of where they run and how they handle concurrency.
async def:
- Runs directly on the main event loop (main thread)
- Must use
awaitfor I/O operations (database, HTTP calls, file I/O) - Can handle thousands of concurrent requests without blocking
- Blocking code here freezes the entire server
def (sync):
- FastAPI runs it in an external threadpool (
anyio.to_thread.run_sync) - Safe to use blocking I/O (synchronous DB drivers,
requestslibrary,time.sleep) - Each request gets its own thread from the pool
- Slight overhead from thread context switching
Decision rule:
- Use
async defwhen your I/O libraries supportawait(httpx, asyncpg, aiofiles) - Use
defwhen using blocking libraries (requests, psycopg2, SQLAlchemy sync)
# Async -- runs on event loop, must await I/O
@app.get("/async")
async def async_endpoint():
result = await async_db_query()
return result
# Sync -- runs in threadpool, blocking is safe
@app.get("/sync")
def sync_endpoint():
result = blocking_db_query()
return result
import asyncio
import time
from fastapi import FastAPI
app = FastAPI()
# Async endpoint -- runs on the event loop
@app.get("/async-items")
async def get_items_async():
# Simulates an async DB query
await asyncio.sleep(0.1) # non-blocking sleep
return {"items": ["widget", "gadget"], "handler": "async"}
# Sync endpoint -- FastAPI runs this in a threadpool
@app.get("/sync-items")
def get_items_sync():
# Simulates a blocking DB query
time.sleep(0.1) # blocking sleep -- safe here because threadpool
return {"items": ["widget", "gadget"], "handler": "sync"}
# DANGER: blocking inside async def freezes the event loop
@app.get("/bad-async")
async def bad_async_endpoint():
# DO NOT DO THIS -- blocks the entire event loop
# time.sleep(5) # would freeze ALL async handlers for 5 seconds
await asyncio.sleep(0.01) # use await instead
return {"warning": "never block in async def"}
# Demonstrating the difference
async def demo():
"""Show that async enables concurrency."""
async def fake_api_call(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"{name} done"
# Sequential -- takes 0.3s total
start = time.time()
r1 = await fake_api_call("A", 0.1)
r2 = await fake_api_call("B", 0.1)
r3 = await fake_api_call("C", 0.1)
sequential_time = time.time() - start
# Concurrent -- takes ~0.1s total
start = time.time()
r1, r2, r3 = await asyncio.gather(
fake_api_call("A", 0.1),
fake_api_call("B", 0.1),
fake_api_call("C", 0.1),
)
concurrent_time = time.time() - start
print(f"Sequential: {sequential_time:.2f}s")
print(f"Concurrent: {concurrent_time:.2f}s")
print(f"Speedup: {sequential_time / concurrent_time:.1f}x")
asyncio.run(demo())
- •async def runs on the event loop; def runs in an external threadpool
- •Use async def when I/O libraries support await; use def for blocking libraries
- •Blocking code in async def freezes the entire server -- the most common FastAPI mistake
- •FastAPI's threadpool for sync functions adds slight overhead but is safe for blocking I/O
- •time.sleep() or requests.get() inside async def blocks ALL concurrent async handlers
- •The threadpool for sync endpoints has a default size limit (usually 40 threads) -- can be exhausted under heavy load
- •Mixing sync and async in the same codebase is fine but requires understanding which runs where
- •async def without any await inside still runs on the event loop, not the threadpool