Lesson 1 - async and Concurrency

Welcome to async and Concurrency

FastAPI is famously fast, and a big reason is concurrency — its ability to handle many requests at once without spawning a thread for each. The engine behind that is Python’s async/await. You may have written async def endpoints already without thinking hard about it; this lesson explains what it actually does, why it makes a server scale, and — just as important — when it doesn’t help so you don’t reach for it blindly. Get the mental model right and the rest of this module (background tasks, streaming, WebSockets) makes much more sense.

This lesson is mostly conceptual, grounded in two small endpoints you can run.

By the end of this lesson, you will be able to:

  • Explain what the event loop does during an I/O wait
  • Describe what async def and await buy you
  • Choose between async def and def for an endpoint
  • Recognize I/O-bound vs. CPU-bound work

You’ll build on everything from the course so far. Let’s begin.


The Problem: Waiting Wastes a Worker

Most of what an API does is wait: for a database query, an external API call, a file read. In a simple blocking model, a worker that’s waiting on one request can’t do anything else — it just sits there until the wait ends, while other requests queue up behind it. Under load, that’s slow.

Concurrency fixes this by letting one worker make progress on many requests by switching away from any request that’s currently waiting:

A comparison of two execution models. Top, Blocking (sync, one worker): request R1 waits on the database and holds the worker, while R2 and R3 queue and wait their turn — slow under load. Bottom, Async (await on I/O): R1 starts, then while R1 is awaiting (idle), the same worker runs R2 and R3 in that gap, then returns to finish R1. A note explains to use async def with await when an endpoint waits on I/O, while a normal def is fine for CPU work or blocking libraries since FastAPI runs it in a threadpool.
Use async/await when an endpoint waits on I/O; a normal def is fine for CPU work, which FastAPI runs in a threadpool.

The key is the bottom row: during R1’s wait, the worker isn’t idle — it serves R2 and R3, then comes back to finish R1. That’s how a single process handles thousands of concurrent connections.


What async/await Actually Do

An async def function is a coroutine — a function that can pause itself at an await and hand control back to the event loop (the scheduler at the heart of async Python). When you write await some_io(), you’re saying: “this might take a while; if it does, go run something else and resume me when it’s ready.” The event loop uses those pauses to keep the single worker busy across many requests.

The crucial detail: await only helps when what you’re awaiting is genuinely awaitable I/O — an async database driver, an async HTTP client, asyncio.sleep. Awaiting such a call frees the worker. A normal blocking call (like a synchronous database library or time.sleep) does not yield to the loop, so putting it in an async def would freeze the whole server during the wait. Matching await to truly-async I/O is the whole skill.


FastAPI Runs Both async def and def

Here’s what makes FastAPI pleasant: you can write an endpoint either way, and it does the right thing with each.

import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/sync")
def sync_endpoint():
    return {"style": "sync def"}

@app.get("/async")
async def async_endpoint():
    await asyncio.sleep(0)   # a real awaitable
    return {"style": "async def"}

Both are ordinary endpoints from the caller’s perspective:

from fastapi.testclient import TestClient

client = TestClient(app)
print(client.get("/sync").json())
print(client.get("/async").json())
{'style': 'sync def'}
{'style': 'async def'}

The difference is how FastAPI runs them. An async def endpoint runs directly on the event loop — so if it awaits real async I/O, the worker stays free for other requests. A plain def endpoint is run in an external threadpool, so a blocking call inside it can’t freeze the event loop. Either way you get concurrency; you just pick the model that fits your code.

Don’t put blocking calls in an async def

The one trap to avoid: calling a blocking function (a synchronous DB query, time.sleep, heavy CPU work) directly inside an async def. That blocks the event loop and stalls every other request. If your code is blocking and you can’t await it, use a normal def endpoint (FastAPI runs it in the threadpool) — or offload the work. When in doubt, a plain def is the safe default.


When to Use Which

A simple rule covers almost every case:

  • Use async def when the endpoint mostly waits on I/O you can await — an async database driver, calling another service with an async HTTP client, streaming. This is where async shines.
  • Use a plain def when your code is blocking (a sync library) or does CPU-bound work (parsing, number crunching). FastAPI runs it in a threadpool so it won’t block the loop.

Two terms make this precise. I/O-bound work spends its time waiting (network, disk, database) — async helps a lot. CPU-bound work spends its time computing — async doesn’t help (the CPU is busy, not waiting), and heavy CPU work belongs in a background worker or separate process anyway. Pick the style that matches the work, and don’t sprinkle async on everything: it only pays off when there’s a wait to overlap.


Practice Exercises

Exercise 1: I/O-bound or CPU-bound?

Classify each and say whether async def would help: (a) fetching data from a third-party REST API, (b) resizing a large image in memory, (c) querying a database.

Hint

(a) and (c) are I/O-bound (waiting on network/database) — async def with an async client/driver helps. (b) is CPU-bound (the CPU is busy computing, not waiting) — async won’t speed it up; it belongs in a background worker or threadpool.

Exercise 2: Spot the mistake

A developer writes async def get_user(): time.sleep(5); return ... and the whole server becomes unresponsive for five seconds per call. What went wrong?

Hint

time.sleep is a blocking call placed inside an async def, so it blocks the event loop instead of yielding — freezing every other request. The fix is either await asyncio.sleep(5) (a real awaitable) or making the endpoint a plain def so FastAPI runs it in the threadpool.

Exercise 3: Why not async everywhere?

If async is so good for scaling, why not make every endpoint async def?

Hint

Async only helps when there’s an await on real I/O to overlap. An async def with no real await gains nothing, and one with a blocking call inside is actively harmful. For blocking or CPU-bound code, a plain def (run in a threadpool) is simpler and safer. Match the tool to the work.


Summary

FastAPI’s speed comes from concurrency: during an I/O wait, the event loop lets one worker make progress on other requests instead of sitting idle. You unlock that with async def endpoints that await genuinely async I/O — an async database driver, an async HTTP client, streaming. FastAPI also runs plain def endpoints in a threadpool, so blocking and CPU-bound code stays safe without freezing the loop. The rule: async def for awaitable I/O, plain def otherwise — and never put a blocking call inside an async def.

Key Concepts

  • Concurrency — handling many requests by overlapping their I/O waits.
  • Event loop — the scheduler that switches between coroutines at each await.
  • async def / await — a coroutine that pauses at I/O so the worker can do other work.
  • Threadpool — where FastAPI runs plain def endpoints, keeping blocking code off the loop.
  • I/O-bound vs. CPU-bound — waiting work (async helps) vs. computing work (it doesn’t).

Why This Matters

Concurrency is why a single FastAPI process can serve thousands of simultaneous users — and why FastAPI is a popular choice for high-throughput services. Understanding when async helps (and the classic “blocking call in an async def” trap) is what separates an API that scales gracefully from one that mysteriously stalls under load. This model also underpins the rest of the module: background tasks, streaming, and WebSockets all build on the event loop you just met.


Next Steps

Continue to Lesson 2 - Background Tasks

Run work after the response is sent — sending emails, writing logs, cleanup — without making the client wait.

Back to Module Overview

Return to the Async, Background Work, and Streaming module overview


Continue Building Your Skills

You now understand what async really does and when to use it — the foundation for everything else in this module. Next you’ll put work after the response with background tasks, so a client gets an instant reply while slower jobs finish in the background.