Chapter 16 · Async Concurrency and Background Tasks
Chapter goals: Understand the true execution model of the event loop and
def/async defpath functions, know when you must writeasync, and use BackgroundTasks to defer slow work until after the response.
16.1 Concurrency ≠ Parallelism
- Concurrency: one person (one thread/event loop) switches to task B while waiting for A — ideal for I/O-bound work (network, disk, database);
- Parallelism: multiple people working simultaneously — ideal for CPU-bound work (heavy computation).
Web application latency is almost entirely I/O, so FastAPI's async model delivers enormous benefit: one event loop thread can handle hundreds of other requests during the "wait for database" idle time.
16.2 The True Execution Model of def vs async def
This is the most commonly misunderstood concept in FastAPI. The official decision table:
| Your situation | Path function style |
|---|---|
Called library requires await | Must async def |
| Calling a blocking library (most DB drivers, requests) | Use plain def |
| No external communication at all | async def |
| Unsure | Use plain def |
Underlying mechanism:
import time
from fastapi import FastAPI
app = FastAPI()
# Pattern 1: async def calling a blocking function —— WRONG!
@app.get("/bad/")
async def bad():
time.sleep(3) # Blocks the entire event loop; all concurrent requests freeze
return {"ok": True}
# Pattern 2: plain def —— FastAPI auto-dispatches to thread pool, event loop unaffected
@app.get("/sync-endpoint/")
def sync_endpoint():
time.sleep(3) # Only occupies one thread pool thread
return {"ok": True}
# Pattern 3: async def + genuine async library —— does not block at all
@app.get("/good/")
async def good():
await asyncio.sleep(3) # Event loop schedules other requests during this wait
return {"ok": True}Rule summary:
async deffunctions run in the main event loop. Any blocking call inside (time.sleep, synchronous DB,requests.get) zero出 the app's concurrency instantly;- Plain
defpath functions are dispatched by FastAPI to an external thread pool (Starlette via anyio's to_thread) — the event loop is unaffected; - Both styles can be mixed freely; FastAPI handles each correctly.
The same applies to dependencies
Dependencies follow the same rule: do not make blocking calls in async def dependencies; sync dependencies go to the thread pool.
16.3 Background Tasks: Do the Slow Work After the Response
Sending emails, writing logs, generating reports — the client does not need to wait. BackgroundTasks lets you continue executing after the response is sent:
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def write_notification(email: str, message: str = ""):
with open("log.txt", mode="a") as f:
f.write(f"notification for {email}: {message}\n")
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
# First arg is the function; subsequent args passed positionally/kwarg
background_tasks.add_task(write_notification, email, message="welcome")
return {"message": "Notification sent in the background"}Key points:
- Declare
BackgroundTasksdirectly as a parameter type; FastAPI auto-injects it; - Task functions can be
deforasync def; FastAPI executes both correctly (sync tasks run in the thread pool); - Multiple
.add_task()calls register multiple tasks; they execute in registration order; - Execution timing: after the response is sent, and after all middleware completes.
Complete example with multiple tasks:
@app.post("/orders/{order_id}/receipt")
async def send_receipt(order_id: int, background_tasks: BackgroundTasks):
# Tasks execute in registration order: audit log first, then email, then metric
background_tasks.add_task(write_audit_log, order_id)
background_tasks.add_task(send_email,
to=f"user-{order_id}@example.com",
template="receipt") # Keyword args pass through
background_tasks.add_task(increment_metric, "receipts_sent")
return {"status": "accepted"} # HTTP 202 is more semantically accurate: accepted, not complete16.4 Chapter Summary
- Concurrency (I/O-bound) vs parallelism (CPU-bound): FastAPI excels at the former;
async defruns in the event loop — never block it; plaindefgoes to the thread pool;BackgroundTasksdefers slow work until after the response is sent;- Mix sync and async freely; FastAPI handles the execution context.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What happens if you call time.sleep() inside an async def path function?
2. When should you use plain def instead of async def?
3. When do BackgroundTasks actually execute?
4. Can you mix sync and async path functions in the same app?
🛠️ Hands-On Exercises
- Write two routes: one
async defwithawait asyncio.sleep(1), one plaindefwithtime.sleep(1). Send 10 concurrent requests to each; measure total wall-clock time to verify concurrency difference. - Add a
BackgroundTasksto a POST endpoint that writes a log entry after returning 201; verify the response is sent before the log is written. - Deliberately call
time.sleep(5)in anasync defroute; send 5 concurrent requests and observe the serial execution.
Proceed to Chapter 17: WebSocket Real-Time Communication.