I. Why Asynchronous Programming?¶
Before diving into FastAPI’s asynchronous journey, let’s understand the difference between synchronous and asynchronous programming.
Imagine ordering coffee at a café:
- Synchronous: You wait at the counter until your coffee is ready. During this time, the barista can’t serve other customers, making it inefficient.
- Asynchronous: You get a number, sit down, and wait to be called. The barista can serve others while you wait, and you only return when your order is ready.
Core of Asynchronous Programming: Let programs not block the current task while waiting for I/O operations (e.g., database queries, API requests, file reads/writes). This is critical for API services handling high concurrency (e.g., user traffic, data synchronization).
FastAPI’s async support enables high efficiency in I/O-bound scenarios (e.g., database interactions, third-party API calls).
II. Asynchronous Fundamentals in FastAPI¶
2.1 Defining Asynchronous Functions¶
In FastAPI, define async views with async def and use await when calling async functions.
from fastapi import FastAPI
app = FastAPI()
# Async view function
@app.get("/")
async def read_root():
return {"message": "Hello, Async FastAPI!"} # No await needed for non-async code
Note: When there are no async I/O operations, async def behaves like a regular def, but async functions allow await internally.
2.2 Using await to Call Async Functions¶
If your function calls another async function (e.g., an async database query), use await to wait for completion.
import asyncio
async def async_database_query():
await asyncio.sleep(1) # Simulate 1-second async database query
return {"user": "Alice", "age": 30}
@app.get("/user")
async def get_user():
result = await async_database_query() # Must use await for async calls
return result
III. Differences Between Async and Sync Views¶
| Type | Definition | Use Case | Key Characteristics |
|---|---|---|---|
| Sync View | def |
Simple logic, CPU-bound tasks | Executes directly without event loop blocking |
| Async View | async def |
I/O-bound tasks (DB, APIs) | Non-blocking, handles multiple requests concurrently |
Example Comparison:
# Synchronous view (using requests for external API)
import requests
from fastapi import FastAPI
app = FastAPI()
@app.get("/sync-data")
def sync_data():
response = requests.get("https://api.example.com/data") # Blocks during request
return response.json()
# Asynchronous view (using aiohttp for external API)
import aiohttp
from fastapi import FastAPI
app = FastAPI()
@app.get("/async-data")
async def async_data():
async with aiohttp.ClientSession() as session: # Async session
async with session.get("https://api.example.com/data") as response:
return await response.json() # Await response
IV. Asynchronous Database Operations¶
FastAPI requires async database drivers/ORMs for non-blocking I/O. Here are two common approaches:
4.1 Using SQLAlchemy (v1.4+ Async Support)¶
SQLAlchemy 1.4+ supports async operations with drivers like asyncpg (PostgreSQL) or aiosqlite (SQLite).
Step 1: Install Dependencies
pip install sqlalchemy asyncpg # For PostgreSQL
Step 2: Configure Async Session
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
# Dependency to get async DB session
async def get_db():
async with AsyncSessionLocal() as session:
yield session
await session.commit()
Step 3: Async Query Example
from sqlalchemy import select
from sqlalchemy.orm import Session
from fastapi import Depends, FastAPI
app = FastAPI()
class User(Base): # Base = SQLAlchemy declarative base
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).filter(User.id == user_id))
user = result.scalars().first()
return user or {"error": "User not found"}
4.2 Using Tortoise-ORM (Simpler Async ORM)¶
Tortoise-ORM is designed for async-first use cases with cleaner syntax.
Install Dependency:
pip install tortoise-orm
Define Models & Queries:
from tortoise import fields, Tortoise
from tortoise.contrib.fastapi import register_tortoise
from fastapi import FastAPI
app = FastAPI()
class User(Tortoise.Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
# Initialize Tortoise ORM
register_tortoise(
app,
db_url="sqlite://db.sqlite3",
modules={"models": ["__main__"]},
generate_schemas=True, # Auto-creates tables
)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await User.get(id=user_id) # Async query
return {"id": user.id, "name": user.name}
V. Asynchronous Task Handling¶
FastAPI’s async capabilities extend beyond API requests to background tasks (e.g., sending emails, generating reports).
5.1 Using asyncio for Simple Async Tasks¶
import asyncio
from fastapi import FastAPI
app = FastAPI()
async def send_email_task(to: str, content: str):
await asyncio.sleep(2) # Simulate email sending delay
return f"Email sent to {to}"
@app.post("/send-email")
async def send_email(to: str, content: str):
task = asyncio.create_task(send_email_task(to, content)) # Non-blocking
return {"status": "Email queued", "task_id": id(task)} # Task ID for demonstration
5.2 Advanced: Async Task Queues (e.g., Celery + Redis)¶
For long-running tasks (e.g., batch processing), use an async task queue:
from fastapi import FastAPI
from celery import Celery
import time
app = FastAPI()
# Initialize Celery (configure Redis/RabbitMQ as broker)
celery = Celery(
"tasks",
broker="redis://localhost:6379/0",
backend="redis://localhost:6379/0"
)
@celery.task
def long_running_task():
time.sleep(5) # Simulate CPU-bound work
return "Task completed"
@app.post("/run-task")
async def run_task():
task = long_running_task.delay() # Send to queue
return {"task_id": task.id, "status": "running"}
VI. Practical Considerations¶
6.1 Avoid Blocking the Event Loop¶
- Never use
time.sleep(): Blocks the entire event loop, preventing other requests from being handled.
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/bad-sleep")
async def bad_sleep():
time.sleep(5) # ❌ Blocks event loop!
return {"error": "This is bad"}
@app.get("/good-sleep")
async def good_sleep():
await asyncio.sleep(5) # ✅ Non-blocking
return {"success": "This is good"}
- Stay away from CPU-bound operations: Async is for I/O, not CPU-heavy work (use multiprocessing instead).
6.2 Efficiently Using asyncio Tasks¶
- Parallelize tasks with
asyncio.gather():
async def task1():
await asyncio.sleep(1)
return "Task 1 done"
async def task2():
await asyncio.sleep(2)
return "Task 2 done"
@app.get("/parallel-tasks")
async def parallel_tasks():
results = await asyncio.gather(task1(), task2()) # Total time ≈ 2s
return {"results": results}
VII. Complete Async Application Example¶
from tortoise import fields, Tortoise
from tortoise.contrib.fastapi import register_tortoise
from fastapi import FastAPI
app = FastAPI()
# Define Tortoise Model
class User(Tortoise.Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
age = fields.IntField()
# Initialize Tortoise ORM
register_tortoise(
app,
db_url="sqlite://async_demo.db",
modules={"models": ["__main__"]},
generate_schemas=True,
)
# Async DB Operations
@app.post("/users")
async def create_user(name: str, age: int):
user = await User.create(name=name, age=age)
return {"id": user.id, "name": user.name, "age": user.age}
@app.get("/users")
async def get_users():
users = await User.all()
return [{"id": u.id, "name": u.name, "age": u.age} for u in users]
# Async Task Handling
import asyncio
async def async_task(delay: int):
await asyncio.sleep(delay)
return f"Task done after {delay}s"
@app.post("/start-task")
async def start_task(delay: int = 2):
task = asyncio.create_task(async_task(delay))
return {"task_id": id(task), "status": "started"}
VIII. Summary¶
FastAPI’s async programming enhances I/O-bound applications’ concurrency. Key takeaways:
- Async Views: Use
async def+awaitto avoid blocking the event loop. - Async Databases: Prefer SQLAlchemy 1.4+ or Tortoise-ORM for database interactions.
- Task Processing: Use
asynciofor small tasks and task queues for large operations. - Avoid Pitfalls: Never block with
time.sleep(); offload CPU work to multiprocessing.
With these fundamentals, you’re ready to build high-performance async applications in FastAPI!