Search Topics
Search across all FastAPI topics
The Event Loop
Under the HoodOne line of code froze your entire API. Every endpoint, every user, everything. Let's understand why.
You add time.sleep(10) to an async endpoint for "testing". Not just that endpoint — your ENTIRE API freezes for 10 seconds. Every single endpoint, every connected user. One line of code took down everything.
@app.get("/slow")
async def slow_endpoint():
time.sleep(10) # "Just for testing!"
return {"status": "done"}
# Meanwhile, at the SAME time:
GET /health → ... hanging
GET /users → ... hanging
GET /items → ... hanging
GET / → ... hanging
# ALL endpoints frozen. Not just /slow.
# A single time.sleep() blocked the entire event loop.Question
How can one time.sleep(10) in one endpoint freeze every other endpoint? The answer is the event loop — the single thread that powers all of your async code. Block it, and everything stops.
The Chef Analogy
Think of a chef in a kitchen. A synchronous chef cooks one dish completely before starting the next — if something needs to simmer for 10 minutes, they just stand there waiting.
An async chef (the event loop) is smarter. When dish 1 goes in the oven, they start preparing dish 2. When dish 2 needs to marinate, they check on dish 1. One chef, many dishes, no idle time.
But here's the catch: there's only ONE chef. If that chef gets stuck stirring a pot and can't let go (that's your time.sleep), every other dish burns. Every customer waits. The whole kitchen is frozen.
Event loop picks up task
Your request
Runs until await
Does work, then yields
Switches to next task
While first awaits I/O
I/O completes
Resumes original task
the event loop model
What you just learned
The event loop is a single thread that juggles all your async tasks
When a task hits 'await', it pauses and the loop runs something else
If anything blocks without awaiting (like time.sleep), the entire loop freezes
Event Loop Visualized
Watch how the event loop handles five concurrent requests using a single thread. No parallelism needed — just smart task switching.
def vs async def — What Actually Happens
FastAPI treats these two differently. With def, FastAPI is cautious — it runs your handler in a thread pool so blocking is safe. With async def, FastAPI trusts you — it runs your handler directly on the event loop. Break that trust and you freeze everything.
# Synchronous — runs in a thread pool (safe to block)
@app.get("/sync")
def get_users():
users = db.fetch_all() # Blocks this thread, not the event loop
return users
# Asynchronous — runs on the event loop (NEVER block!)
@app.get("/async")
async def get_users():
users = await db.fetch_all() # Yields to event loop while waiting
return userssync vs async execution
What you just learned
def endpoints run in a thread pool — blocking is safe but uses more resources
async def endpoints run on the event loop — lightweight but must never block
The event loop trusts your async code to yield. Break that trust and everything stops.
What Happens at Every await
When your code hits await, the event loop pauses your function and is free to run other tasks. When the I/O completes, the loop picks up right where you left off.
@app.get("/dashboard")
async def get_dashboard():
# 1. Event loop starts running this function
user = await get_current_user()
# 2. Pauses here → event loop handles other requests
# 3. User data arrives → resumes right here
posts = await db.fetch_posts(user.id)
# 4. Pauses again → event loop handles other requests
# 5. Posts arrive → resumes right here
return {"user": user, "posts": posts}
# 6. Done. Event loop moves to the next task.Go Deeper: CPU-Bound Work
Not all blocking is I/O. Heavy computation (image processing, ML inference) also blocks the event loop because there's no await to yield. The fix: offload it to a thread pool.
# BAD: CPU work blocks the event loop
@app.get("/process")
async def process_image():
result = heavy_computation() # No await — freezes everything!
return result
# GOOD: Offload to a thread pool
from fastapi.concurrency import run_in_threadpool
@app.get("/process")
async def process_image():
result = await run_in_threadpool(heavy_computation)
return result # Event loop stayed free the whole timeInsight
Or just use def for the endpoint! FastAPI will run it in a thread pool automatically. No need for the run_in_threadpool dance if your entire endpoint is synchronous work.
Think about it...
If the event loop is single-threaded, how can FastAPI handle multiple requests concurrently?
Hint: Think about what happens between the request arriving and the I/O completing.
Key Points
Single Thread
The event loop runs on one thread — no race conditions, no locks
await = Yield
Each await lets the event loop switch to another task while waiting
I/O Bound = Great
Async shines with network calls, DB queries, file I/O
CPU Bound = Thread Pool
For heavy computation, use run_in_threadpool to avoid blocking
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
import time
@app.get("/slow")
async def slow_endpoint():
time.sleep(3) # Blocks the entire event loop!
# No other request can be processed for 3 seconds
return {"status": "done"}import requests
@app.get("/fetch")
async def fetch_data():
# This blocks the event loop!
resp = requests.get("https://api.example.com/data")
return resp.json()