Skip to content

Chapter 16 · Async Concurrency and Background Tasks ​

Chapter goals: Understand the true execution model of the event loop and def/async def path functions, know when you must write async, 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 situationPath function style
Called library requires awaitMust async def
Calling a blocking library (most DB drivers, requests)Use plain def
No external communication at allasync def
UnsureUse plain def

Underlying mechanism:

python
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 def functions run in the main event loop. Any blocking call inside (time.sleep, synchronous DB, requests.get) zero出 the app's concurrency instantly;
  • Plain def path 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:

python
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 BackgroundTasks directly as a parameter type; FastAPI auto-injects it;
  • Task functions can be def or async 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:

python
@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 complete

16.4 Chapter Summary ​

  • Concurrency (I/O-bound) vs parallelism (CPU-bound): FastAPI excels at the former;
  • async def runs in the event loop — never block it; plain def goes to the thread pool;
  • BackgroundTasks defers 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 ​

  1. Write two routes: one async def with await asyncio.sleep(1), one plain def with time.sleep(1). Send 10 concurrent requests to each; measure total wall-clock time to verify concurrency difference.
  2. Add a BackgroundTasks to a POST endpoint that writes a log entry after returning 201; verify the response is sent before the log is written.
  3. Deliberately call time.sleep(5) in an async def route; send 5 concurrent requests and observe the serial execution.

Proceed to Chapter 17: WebSocket Real-Time Communication.