Background & WebSockets — Cheat Sheet
FastAPI · 3 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Background & WebSockets
FastAPI3 topicsQuick revision reference
1
BackgroundTasks — Work After the Response
BackgroundTasks runs functions after the response is sent — perfect for emails, logs, and webhooks the user shouldn't wait for. Know its limits: in-process, no retries, dies with the worker — that's where Celery/ARQ enter.
- ✓add_task queues work that runs after the response — user never waits for side effects
- ✓Sync tasks → thread pool, async tasks → loop; endpoint latency excludes both
- ✓No persistence, retries, or visibility — a crash silently loses queued tasks
- ✓Critical, slow, or retry-needing work → Celery/ARQ with 202 + job-status URL
Queue side effects, return immediately, run after the response
import logging, time
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
log = logging.getLogger("tasks")
def send_welcome_email(email: str, name: str):
time.sleep(2) # SMTP being SMTP (sync is fine here —
log.info("welcome mail sent to %s", email) # it runs in the thread pool)
def audit(event: str, **fields):
log.info("AUDIT %s %s", event, fields)
@app.post("/register", status_code=201)
def register(payload: dict, background: BackgroundTasks):
user = {"id": 101, "email": payload["email"], "name": payload["name"]}
# ... hash password, insert user (the REQUEST work) ...
background.add_task(send_welcome_email, user["email"], user["name"])
background.add_task(audit, "user.registered", user_id=user["id"])
return {"id": user["id"]} # ← returns NOW; email sends after
# Timeline:
# 0ms request in → user created
# 15ms 201 response SENT — user's spinner is gone
# 15ms+ send_welcome_email runs (2s), then audit — user never waited
#
# Works inside dependencies too (e.g., a usage-metering dependency that
# add_tasks a counter bump for every call). Same BackgroundTasks object
# is shared between dependencies and the endpoint.2
WebSockets — Real-Time, Two-Way Connections
A @app.websocket endpoint accepts a persistent two-way connection: await receive/send in a loop, a ConnectionManager broadcasts to rooms, auth happens at the handshake, and multi-instance fan-out needs Redis pub/sub.
- ✓WS = HTTP upgrade to persistent full-duplex; endpoint is an accept + receive/send loop
- ✓ConnectionManager tracks rooms; always clean up on WebSocketDisconnect
- ✓Auth at the handshake (query-param ticket / first message) — reject with 1008 before accept
- ✓Sockets are per-instance: multi-pod broadcast requires Redis pub/sub between pods
accept → receive/send loop → WebSocketDisconnect cleanup
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.rooms: dict[str, set[WebSocket]] = {} # room → live sockets
async def connect(self, room: str, ws: WebSocket):
await ws.accept() # completes the handshake
self.rooms.setdefault(room, set()).add(ws)
def disconnect(self, room: str, ws: WebSocket):
self.rooms.get(room, set()).discard(ws) # ALWAYS clean up
async def broadcast(self, room: str, message: dict):
dead = []
for ws in self.rooms.get(room, set()):
try:
await ws.send_json(message)
except Exception:
dead.append(ws) # died mid-broadcast
for ws in dead:
self.disconnect(room, ws)
manager = ConnectionManager()
@app.websocket("/ws/drives/{drive_id}/chat")
async def drive_chat(ws: WebSocket, drive_id: str):
await manager.connect(drive_id, ws)
await manager.broadcast(drive_id, {"sys": "someone joined"})
try:
while True: # the connection's life
data = await ws.receive_json() # blocks until a message
await manager.broadcast(drive_id, {
"from": data.get("name", "anon"),
"text": data["text"],
})
except WebSocketDisconnect: # tab closed, network died
manager.disconnect(drive_id, ws)
await manager.broadcast(drive_id, {"sys": "someone left"})
# Browser side:
# const ws = new WebSocket("wss://api.example.com/ws/drives/7/chat")
# ws.onmessage = (e) => render(JSON.parse(e.data))
# ws.send(JSON.stringify({name: "Asha", text: "When is the TCS drive?"}))3
Streaming & SSE — Pushing Data as It Happens
StreamingResponse sends bytes as a generator yields them — huge CSV exports without RAM spikes, and Server-Sent Events (text/event-stream) for one-way push: the protocol behind every LLM token stream.
- ✓StreamingResponse flushes generator chunks immediately — constant memory for huge responses
- ✓SSE = text/event-stream with "data: ...\n\n" frames; EventSource auto-reconnects
- ✓X-Accel-Buffering: no (and proxy config) — buffering proxies silently kill streams
- ✓One-way push (tokens, progress, notifications) → SSE; two-way interaction → WebSocket
Yield chunks, flush immediately — 200MB export, 1-row memory
import csv, io
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
# ✗ The naive export: builds ALL rows in RAM, ships once
# @app.get("/export") → 4 lakh students × 500B = 200MB string + timeout risk
# ✓ Streaming: constant memory, first bytes arrive immediately
@app.get("/students/export.csv")
async def export_students():
async def rows():
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["id", "name", "branch", "cgpa"]) # header
yield buf.getvalue(); buf.seek(0); buf.truncate(0)
async for student in iter_students_in_batches(1000): # keyset pages!
writer.writerow([student.id, student.name,
student.branch, student.cgpa])
yield buf.getvalue(); buf.seek(0); buf.truncate(0)
return StreamingResponse(
rows(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=students.csv"},
)
# Memory: one row's worth, forever. The DB pagination inside is the
# keyset pattern from the Databases chapters — OFFSET would re-scan.
# Same tool proxies big files without buffering:
# return StreamingResponse(s3_object.iter_chunks(),
# media_type="application/pdf")Learn this free with Aria, your AI tutor → AiCanCode.org/learn/fastapi