Async routes

async def vs. plain def routes

A route can be declared either way:

@app.get("/sync-agent")
def get_agent_sync():
    return {"name": "research_agent"}

@app.get("/async-agent")
async def get_agent_async():
    return {"name": "research_agent"}

Both work, and for a route like this — no actual waiting involved — there’s no real difference. The distinction only matters once a route actually does I/O-bound waiting: calling an LLM API, another service, or awaiting anything else inside the route function.

Where it actually matters

import asyncio

async def call_llm_api(prompt: str) -> str:
    await asyncio.sleep(7)   # standing in for a real network call
    return f"response to: {prompt}"

@app.post("/generate")
async def generate(prompt: str):
    result = await call_llm_api(prompt)
    return {"result": result}
POST /generate
click Run to see this pane's output

async def generate can await call_llm_api(...) directly — exactly the coroutine-calling-coroutine pattern from the async lesson. While this one request is waiting on call_llm_api, FastAPI’s event loop is free to handle other incoming requests concurrently — the entire reason this course built up asyncio in the first place, now paying off directly inside route handling.

The trap: a blocking call inside async def

This is the exact poisoning behavior from the async lesson, now in a context where it’s easy to hit by accident — a route declared async def that calls a genuinely blocking function (a synchronous database driver, time.sleep(), a non-async HTTP library) blocks the entire server, not just that one request:

import time

@app.post("/generate-blocking")
async def generate_blocking(prompt: str):
    time.sleep(7)   # blocking — freezes every other request too
    return {"result": f"response to: {prompt}"}

Two clients, hitting this server at the same moment — one calling the broken route, the other just checking /health, something with nothing to do with /generate-blocking at all:

Client A: POST /generate-blocking
click Run to see this pane's output
Client B: GET /health, sent at the same moment
click Run to see this pane's output

/health does nothing and depends on nothing — it still took the full seven seconds, because one blocking call poisons the entire event loop, the same way it poisoned an entire gather() in the async lesson. The rule from that lesson applies unchanged: never call a blocking function from inside async def code.

The fix, if a route genuinely needs blocking code

If a route needs to call something blocking that has no async alternative, the fix is simply not declaring it async def — FastAPI automatically runs a plain def route in a separate thread pool, rather than directly on the event loop:

@app.post("/generate-safe")
def generate_safe(prompt: str):
    time.sleep(7)   # blocking, but this route isn't async def —
    return {"result": f"response to: {prompt}"}   # FastAPI runs it in a thread pool instead

Same two clients, same moment, only this route changed:

Client A: POST /generate-safe
click Run to see this pane's output
Client B: GET /health, sent at the same moment
click Run to see this pane's output

/health comes straight back this time — the blocking work still takes its full seven seconds, but off on its own thread, not on the shared event loop /health needed to run on. This route still isn’t concurrent with itself the way await-ing a real async call would be, but it no longer blocks the entire server the way the broken async def version did.

The practical rule: declare a route async def only when it actually awaits something — an async database driver, an async HTTP client, another coroutine. If a route’s logic is entirely synchronous (plain, blocking calls, no await anywhere in its body), leave it a plain def and let FastAPI’s thread pool handle it safely, rather than declaring async def out of habit and accidentally introducing the poisoning trap.

Check your understanding
1/5

For a route with no actual waiting involved (just returning a value immediately), does it matter whether it's declared async def or plain def?