Async Endpoints & Concurrency
Part of: Advanced FastAPI Patterns
Understand the difference between sync and async endpoints in FastAPI, and learn when to use each for optimal performance.
What You'll Learn
- Understand the difference between def and async def in FastAPI
- Know when to use sync vs async path operation functions
- Use asyncio.sleep to simulate non-blocking async work
- Grasp how the event loop handles concurrent requests
Theory and Concepts
Async Endpoints & Concurrency
What You'll Learn
In this lesson you will explore one of FastAPI's most powerful features: native support for asynchronous Python. You will learn:
- The difference between def and async def path operation functions
- How the Python event loop manages concurrent requests
- When to choose sync vs async endpoints
- How to perform non-blocking work with asyncio
Theory
Sync vs Async in Python
Python supports two styles of function definitions:
[Code Example]
The async def keyword declares a coroutine. Inside a coroutine you can use await to pause execution while waiting for an I/O operation, allowing other tasks to run in the meantime.
How FastAPI Handles Each Type
FastAPI treats def and async def endpoints differently:
Plain def endpoints are run in a thread pool. FastAPI knows that a regular function might contain blocking I/O (like reading a file or calling an external API with a synchronous library), so it offloads the function to a separate thread to avoid blocking the event loop.
[Code Example]
async def endpoints run directly on the event loop. They must not contain blocking operations, but they can use await for non-blocking I/O.
[Code Example]
The Event Loop
The event loop is the core of Python's asyncio framework. Think of it as a task scheduler:
1. It picks up a task and runs it until it hits an await
2. When a task awaits, the loop suspends it and picks up the next ready task
3. When the awaited operation completes, the task is resumed
This means a single thread can handle thousands of concurrent requests, as long as each request spends most of its time waiting for I/O rather than computing.
Simulating Async Work
You can simulate a non-blocking delay with asyncio.sleep():
[Code Example]
Unlike time.sleep() (which blocks the thread), asyncio.sleep() yields control back to the event loop, allowing other requests to be processed during the wait.
Key Concepts
When to Use async def
Use async def when your endpoint:
- Calls other async functions with await
- Uses an async database driver (e.g., asyncpg, motor)
- Uses an async HTTP client (e.g., httpx.AsyncClient)
- Performs no blocking I/O at all (simple return)
When to Use Plain def
Use plain def when your endpoint:
- Calls synchronous blocking libraries
- Performs CPU-bound computation
- Uses synchronous database drivers (e.g., psycopg2)
- Reads or writes files using standard open()
Common Mistake: Blocking in async def
Never do this:
[Code Example]
This would freeze all other requests for 5 seconds because time.sleep() blocks the event loop thread. Use asyncio.sleep() or move blocking work to a plain def endpoint.
Performance Implications
For simple endpoints that just return data without any I/O:
- async def is slightly faster (no thread pool overhead)
- def still works perfectly fine
For I/O-heavy endpoints:
- async def with proper await calls gives the best concurrency
- def works but each request occupies a thread from the pool
Best Practices
1. Default to async def for simple endpoints that perform no blocking I/O
2. Use def when you must call synchronous blocking libraries
3. Never mix blocking calls inside async def -- move them to def or use asyncio.to_thread()
4. Use await for every async operation -- forgetting await is a common bug
5. Keep async endpoints lightweight -- offload heavy CPU work to background tasks or workers
6. Test both patterns -- ensure your async endpoints actually return correct responses
Additional Resources
- FastAPI Async Documentation
- Python asyncio Documentation
- Concurrency and async / await in FastAPI
Helpful Hint
Use 'async def' for endpoints that perform async I/O operations (like awaiting a coroutine). Use plain 'def' for CPU-bound or blocking I/O work. Remember to 'await' async calls like asyncio.sleep().
