Skip to main content
Modern web applications routinely wait — for database queries to return, for external API responses, for files to be read from disk. In traditional synchronous code, each waiting operation blocks the entire thread, preventing your server from handling other requests. Python’s asyncio framework with async/await syntax solves this problem by letting a single thread manage thousands of concurrent I/O operations, handing control to the event loop while one task waits so it can immediately start working on another. This is exactly how FastAPI achieves high throughput without requiring multiple threads or processes.

Concurrency vs. Parallelism

Before writing async code, it’s important to distinguish between two related but different concepts: FastAPI is built for I/O-bound concurrency — your server spends most of its time waiting for database queries and external API calls, not crunching numbers.

Async & Await Declarations

Coroutines

Declaring a function with async def creates a coroutine. Calling a coroutine does not run it — it returns a coroutine object. To actually execute it, you must await it:
asyncio.sleep() is the async equivalent of time.sleep(). Unlike time.sleep(), it releases control back to the event loop during the wait, allowing other tasks to run.

The Event Loop and Task Scheduling

The event loop is the engine that drives async programs:
1

Run a task

The event loop starts executing a coroutine.
2

Hit an await

The coroutine reaches an await expression (e.g., a database query or network call).
3

Pause and switch

The loop pauses the current coroutine and picks up another ready task.
4

Resume

When the I/O operation completes, the loop resumes the original coroutine from where it paused.

Running Tasks Concurrently with asyncio.gather()

Without gathering, tasks run sequentially — the total time is the sum of all delays. With asyncio.gather(), tasks run concurrently — the total time is only as long as the slowest task:
Without gather, these three calls would take 2 + 1 + 3 = 6 seconds. With gather, they run concurrently and finish in just 3 seconds — the duration of the slowest call.

create_task() for More Control

When you need to start a coroutine and continue working before its result is ready, use asyncio.create_task():

Why FastAPI Uses Async

FastAPI is built on ASGI (Asynchronous Server Gateway Interface) and natively supports async def route handlers:
When a client request hits this endpoint:
  1. FastAPI calls the coroutine.
  2. The coroutine hits await asyncio.sleep(0.1) (or a real database query).
  3. The event loop pauses this coroutine and immediately starts handling the next incoming request.
  4. When the database responds, the original coroutine resumes and sends its HTTP response.
A single process with a single thread can serve thousands of concurrent requests this way — without the overhead of threads or processes.
Use async def for route handlers that perform I/O — database queries, HTTP calls, file reads. Use regular def for pure CPU computation. FastAPI handles both correctly, but mixing them incorrectly (e.g., calling a blocking time.sleep() inside an async def handler) will block the event loop and hurt performance.
Never call blocking (synchronous) I/O operations directly inside an async def function. Replace time.sleep() with await asyncio.sleep(), and use async database drivers (like asyncpg or SQLAlchemy async) instead of their synchronous counterparts.