Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous
Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Async improves performance mainly for I/O-bound workloads under concurrent load, not for CPU-bound computation.
Briefing
The core takeaway is that FastAPI can run routes either synchronously or asynchronously, but the performance payoff from async only materializes when requests spend time waiting on external I/O (like databases or network calls). For CPU-heavy work, async won’t help and can even add overhead. The practical result in this tutorial is a full conversion of a CRUD-heavy FastAPI app from sync SQLAlchemy sessions to async SQLAlchemy, while keeping behavior the same—especially around relationship loading.
Async is framed as a way to avoid idle time. Instead of handling one task end-to-end (sync), asynchronous code can juggle multiple in-flight requests while waiting on external systems (I/O-bound tasks). The tutorial emphasizes that async doesn’t speed up computation; it helps when the program would otherwise block waiting for database responses, external API calls, or file reads. That distinction matters because it determines whether switching to async is worth the added complexity.
FastAPI’s handling of sync vs async routes is then made concrete. A normal `def` route runs in a separate thread pool so it won’t block the main event loop. An `async def` route runs directly in the event loop, which is more efficient—but only if every I/O operation is properly awaited. If blocking I/O sneaks into an async route (for example, using a synchronous database session or a synchronous HTTP client), it can stall the entire event loop and negate the benefits.
The conversion work starts with database infrastructure. The app moves from SQLAlchemy’s synchronous engine to an async engine using `aiosqlite` as the async driver (via a `+aiosqlite` URL). It replaces the session factory with an async session maker, updates the engine creation to `create_async_engine`, and changes the `get_db` dependency into an async generator using `async with` on the async session. The tutorial also highlights `expire_on_commit=False` as a recommended async setting to avoid problems with expired objects and lazy loading.
Table creation shifts into FastAPI’s `lifespan` function. Because `metadata.create_all` is synchronous, it can’t be called directly with the async engine. Instead, startup uses `engine.begin()` with an async connection and runs the sync create call inside that async context. Shutdown disposes of the engine.
The most critical async SQLAlchemy difference is relationship loading. Lazy loading that works in sync SQLAlchemy fails in async mode (often producing errors like “missing green lit”). The fix is eager loading with `select` and `selectinload` (imported as `select` and `selectinload`). Wherever templates or API responses access related objects—like `post.author`—the queries must include eager loading. For create/update flows, `db.refresh` is used with `attribute_names` (e.g., `author`) so the returned post includes the author relationship without extra queries.
Finally, exception handling is updated to use FastAPI’s default async exception handlers for consistency and less custom code. After installing missing async dependencies (notably `green lit`), the app is tested end-to-end: pages render, API responses include author data, and the external behavior matches the sync version. The tutorial closes by outlining when to use async (multiple concurrent I/O operations, external API/database under load) and when to stick with sync (simple fast operations, CPU work, or when relying on sync-only libraries), while noting that mixed approaches are normal in real systems.
Cornell Notes
Async in FastAPI is most valuable for I/O-bound workloads where requests spend time waiting on external systems like databases or network calls. Sync (`def`) routes run in a thread pool, while async (`async def`) routes run in the event loop and must `await` all I/O to avoid blocking. Converting the app required switching SQLAlchemy to an async engine and session (using `aiosqlite`), moving table creation into a `lifespan` startup routine, and replacing relationship lazy loading with eager loading via `select` + `selectinload`. Because async SQLAlchemy can’t lazy load relationships, queries and `db.refresh(..., attribute_names=...)` must explicitly load related data so API responses and templates still include fields like `post.author`.
Why doesn’t async automatically make an app faster?
How does FastAPI treat `def` routes versus `async def` routes?
What breaks when moving to async SQLAlchemy, and why?
How should relationship data be loaded for templates and API responses in async mode?
Why move table creation into `lifespan` instead of calling `metadata.create_all` directly?
What’s the difference between awaiting `db.commit()`/`db.refresh()` and not awaiting `db.add()`?
Review Questions
- When would switching a FastAPI route from `def` to `async def` likely improve throughput, and when might it not?
- What specific async SQLAlchemy rule forces eager loading, and how do `selectinload` and `db.refresh(..., attribute_names=...)` address it?
- During the async conversion, why is table creation handled in `lifespan`, and what problem would occur if `metadata.create_all` were called directly with the async engine?
Key Points
- 1
Async improves performance mainly for I/O-bound workloads under concurrent load, not for CPU-bound computation.
- 2
FastAPI runs `def` routes in a thread pool, while `async def` routes run in the event loop and must await non-blocking I/O.
- 3
Async SQLAlchemy does not support lazy loading of relationships; eager loading is required to avoid errors like “missing green lit.”
- 4
Switching to async SQLAlchemy involves using an async driver (e.g., `aiosqlite`), `create_async_engine`, an async session maker, and an async `get_db` dependency.
- 5
Table creation must move into an async `lifespan` startup routine because `metadata.create_all` is synchronous.
- 6
Relationship data in responses/templates must be loaded via `select` + `selectinload` and, after writes, via `db.refresh` with `attribute_names`.
- 7
Exception handling should use FastAPI’s default async handlers to keep behavior consistent and reduce custom JSON logic.