Skip to course content
Free FastAPI backend course

FastAPI for AI Backend Development

Unit 11.02: Blocking calls inside an async handler

A blocking call inside an async def handler stalls every other request on that worker.

Five concurrent handlers, serialised

The same wait, blocking and awaited.

The code times both.

import asyncio, time


async def blocking_handler():
    time.sleep(0.05)          # blocks the event loop
    return "done"


async def cooperative_handler():
    await asyncio.sleep(0.05)  # yields to other requests
    return "done"


async def measure(handler, n):
    start = time.perf_counter()
    await asyncio.gather(*(handler() for _ in range(n)))
    return (time.perf_counter() - start) * 1000


n = 5
blocking_ms = asyncio.run(measure(blocking_handler, n))
cooperative_ms = asyncio.run(measure(cooperative_handler, n))
print(f"{n} concurrent handlers, each waiting 50ms")
print(f"  blocking call inside async def : {blocking_ms:>6.0f} ms")
print(f"  awaited properly               : {cooperative_ms:>6.0f} ms")

print("""
The blocking version serialised every request onto one thread. Every other
caller waited, including ones that had nothing to do with this endpoint.

If a library has no async client, declare the handler `def` -- FastAPI then
runs it in a threadpool and the event loop stays free.
""")

The blocking version serialised every request onto one thread. Every other caller waited - including ones hitting completely different endpoints, because they share the event loop.

This is the most consequential mistake in async FastAPI code, and it produces a symptom that looks like a capacity problem rather than a bug.

The mistake this prevents

The mistake is declaring a handler async def and calling a synchronous database driver or HTTP library inside it. Declare it def instead and FastAPI runs it in a threadpool, leaving the loop free.

Takeaway

Never make a blocking call inside async def. If the library has no async client, declare the handler def and let FastAPI use a threadpool.