Async Endpoints & Concurrency
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
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
defandasync defpath 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:
# Synchronous - blocks until complete
def do_work():
return "done"
# Asynchronous - can be paused and resumed
async def do_work():
await some_io_operation()
return "done"
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.
@app.get("/sync/")
def sync_endpoint():
# Runs in a thread pool - safe for blocking I/O
return {"message": "sync"}
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.
@app.get("/async/")
async def async_endpoint():
# Runs on the event loop - use await for I/O
return {"message": "async"}
The Event Loop
The event loop is the core of Python's asyncio framework. Think of it as a task scheduler:
- It picks up a task and runs it until it hits an
await - When a task awaits, the loop suspends it and picks up the next ready task
- 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():
import asyncio
@app.get("/slow/")
async def slow_endpoint():
await asyncio.sleep(0.1) # Non-blocking wait
return {"message": "done"}
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
asyncfunctions withawait - 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:
import time
@app.get("/bad/")
async def bad_endpoint():
time.sleep(5) # BLOCKS the entire event loop!
return {"message": "this is bad"}
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 defis slightly faster (no thread pool overhead)defstill works perfectly fine
For I/O-heavy endpoints:
async defwith properawaitcalls gives the best concurrencydefworks but each request occupies a thread from the pool
Best Practices
- Default to
async deffor simple endpoints that perform no blocking I/O - Use
defwhen you must call synchronous blocking libraries - Never mix blocking calls inside
async def-- move them todefor useasyncio.to_thread() - Use
awaitfor every async operation -- forgettingawaitis a common bug - Keep async endpoints lightweight -- offload heavy CPU work to background tasks or workers
- Test both patterns -- ensure your async endpoints actually return correct responses
Additional Resources
💡 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().
Ready to Practice?
Now that you understand the theory, let's put it into practice with hands-on coding!
Start Interactive Lesson