Module 07: WebSockets and Server-Sent Events
Why this matters
HTTP's request-response model has a built-in limitation: the server can only speak when spoken to. The client asks; the server answers; the connection closes. That's fine for loading a page, but it's the wrong shape for anything live — a chat message someone else just sent, a notification that your background task finished, a stock price ticking, a "user is typing" indicator, a progress bar for a long-running job. For those, the server needs to push data to an already-connected client whenever it has something to say, without the client asking first.
There are three ways to get there, and choosing wrong is a common early mistake. Polling fakes it by asking repeatedly (you saw its waste in module 05). Server-Sent Events (SSE) give you a one-way, server→client stream over plain HTTP. WebSockets give you a full-duplex, bidirectional connection where both sides can send at any time. Knowing which to reach for — and, just as important, how to clean up the connections you open — is the skill here.
That cleanup part is where this module earns its keep. A WebSocket is a long-lived connection holding server resources (memory, a file descriptor, an entry in whatever registry you use to broadcast). Forget to remove a connection when the client vanishes and you have a connection leak: your server slowly accumulates dead connections until it runs out of resources and falls over. This is one of the most common real-world bugs in real-time systems, and this module builds the connection-manager pattern that prevents it — then makes you diagnose the leak when it's missing.
Concepts
Three options: polling, SSE, WebSockets
Polling — the client repeatedly requests updates on a timer. Simple, works everywhere, no special protocol. But it's wasteful (most polls return nothing) and laggy (up to one interval behind). "Long polling" (hold the request open until there's data) reduces lag but ties up a connection per waiting client. Use plain polling only when updates are infrequent and a few seconds of lag is fine.
Server-Sent Events (SSE) — a standardized one-way stream: the client opens
a normal HTTP GET to an endpoint that keeps the response open and writes
events to it over time (text/event-stream). The browser's EventSource API
handles it, including automatic reconnection if the connection drops. SSE
is server→client only — the client can't send over the same channel (it uses
ordinary requests for that). It runs over plain HTTP/HTTPS, so it passes
through proxies and infrastructure easily. Ideal for: notifications, live
feeds, progress updates, dashboards — anything where the server pushes and the
client mostly listens.
WebSockets — a protocol that upgrades an HTTP connection into a persistent, full-duplex channel: both client and server can send messages at any time, independently, until either closes. This is what you want for genuinely interactive, bidirectional, low-latency features: chat, collaborative editing, multiplayer games, live trading. It's more powerful than SSE but also more to manage (no built-in reconnection, a distinct protocol some proxies need configuring for, and you own the connection lifecycle).
The decision rule:
| Need | Use |
|---|---|
| Infrequent updates, lag tolerable, dead simple | Polling |
| Server pushes to client, client just listens | SSE |
| Both sides send, interactive, low latency | WebSockets |
HTTP request/response SSE (one-way stream) WebSocket (full-duplex)
client ──req──► server client ──GET──► server client ◄════════► server
client ◄─resp── server client ◄─event─ server both send anytime,
one shot, then closed ◄─event─ (stays open, persistent, until
◄─event─ server pushes) either side closes
A huge number of features people build with WebSockets ("show me notifications," "update this progress bar") only ever push server→client and would be simpler, more robust, and reconnection-free as SSE. Reach for WebSockets when you genuinely need the client to send over the persistent channel too.
The WebSocket connection lifecycle
A WebSocket connection has distinct phases, and you must handle each:
- Handshake / accept. The client sends an HTTP request with an
Upgrade: websocketheader; the serveraccept()s it, switching the protocol. In FastAPI:await websocket.accept(). - Open / message exchange. Both sides send/receive messages in a loop.
The server typically
await websocket.receive_text()in a loop andawait websocket.send_text(...)whenever it has something to push. - Close / disconnect. Either side closes, or the client vanishes
(browser tab closed, network dropped, laptop slept). A clean close raises
WebSocketDisconnecton the server's next receive; an unclean one is detected on the next send/receive attempt or a ping timeout.
The non-negotiable rule: whatever you did on connect, you must undo on
disconnect, and you must do it in a finally block so it runs no matter
how the connection ended. If you registered the connection in a broadcast
list on accept, you must remove it on disconnect — including the disconnect
you didn't expect.
The connection manager and the leak it prevents
To broadcast (send to many clients), you need a registry of active connections. That registry is the thing that leaks if you're careless. A connection manager centralizes register/unregister/broadcast:
class ConnectionManager:
def __init__(self):
self.active: set[WebSocket] = set()
async def connect(self, ws: WebSocket):
await ws.accept()
self.active.add(ws) # register
def disconnect(self, ws: WebSocket):
self.active.discard(ws) # unregister (idempotent)
async def broadcast(self, message: str):
dead = []
for ws in list(self.active): # copy: we may mutate during iteration
try:
await ws.send_text(message)
except Exception:
dead.append(ws) # sending failed -> it's gone
for ws in dead:
self.disconnect(ws) # prune the ones that failed
broadcast fan-out the leak (missing unregister)
one message ─► registry `active` active = {c1, c2, DEAD, DEAD, c5, DEAD...}
┌─► client 1 (send ok) │ │ │
├─► client 2 (send ok) grows forever; every broadcast
├─► client 3 (send fails) ─► prune still tries the DEAD sockets
└─► client 4 (send ok) until memory/fds run out
The leak happens when a connection is added to active on connect but
not removed when the client disconnects — because you only removed it in the
happy-path branch, or forgot the finally, or assumed clients always close
cleanly. Every dead connection stays in active forever: it consumes memory,
and every broadcast wastes time trying to send to a socket that's gone (or
worse, blocks). Under real traffic — where clients constantly connect and
disappear — active grows without bound until the server dies. The fix is
always the same: remove the connection in a finally (or prune on send
failure), so every exit path unregisters it.
Broadcasting and the multi-process problem
ConnectionManager broadcasts to every client connected to this process.
But production runs multiple worker processes/servers, and a given client is
connected to only one of them. If user A (on process 1) sends a chat message,
process 1's manager can only reach the clients connected to process 1 — users
on process 2 never see it. The solution is a pub/sub backplane (module
08): each process publishes outgoing messages to a shared channel (Redis
Pub/Sub) and subscribes to it, so a message from any process fans out to the
connections on every process. This module builds the single-process manager;
module 08 supplies the backplane that makes it work across processes.
Heartbeats and detecting dead clients
A client can vanish without the OS ever telling your server (a yanked network
cable, a slept laptop). The connection looks open but is dead. Heartbeats
— periodic ping/pong frames — detect this: if a client misses pings, you close
and unregister it, reclaiming the resources and keeping intermediary proxies
from silently timing out an idle connection. WebSocket has built-in ping/pong
frames; SSE conventionally sends a periodic comment line (: keepalive) to
keep the stream and proxies alive. Without heartbeats, dead-but-open
connections are another source of the leak.
Through proxies and load balancers
On the wire, the handshake is a plain HTTP/1.1 request with Connection: Upgrade, Upgrade: websocket, Sec-WebSocket-Key and
Sec-WebSocket-Version: 13; success is 101 Switching Protocols, after which
the same TCP connection carries WebSocket frames instead of HTTP. So a
connection that never opens is failing at an ordinary HTTP step: a 400,
404 or 200 instead of 101 is a readable HTTP error, and the usual header,
status and TLS (wss://) debugging applies.
Two things in front of your app break this in practice:
A reverse proxy that drops the upgrade.
UpgradeandConnectionare hop-by-hop headers, so a proxy doesn't forward them unless told to. In nginx you must set them explicitly, and raise the read timeout above your heartbeat interval, because the default (60s) closes a healthy connection that has simply been quiet:location /ws/ { proxy_pass http://app; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }A load balancer. It picks a backend once, when the connection opens, and the connection stays pinned there for its whole life. That pinning is what makes the multi-process problem above unavoidable once you scale out.
Command reference
| Concern | FastAPI / approach |
|---|---|
| Accept a WebSocket | await websocket.accept() |
| Receive a message | await websocket.receive_text() / receive_json() |
| Send a message | await websocket.send_text(...) / send_json(...) |
| Detect disconnect | catch WebSocketDisconnect on receive |
| Guaranteed cleanup | unregister in a finally block |
| SSE endpoint | return StreamingResponse(gen(), media_type="text/event-stream") |
| SSE event format | data: <payload>\n\n per event |
| SSE reconnection | automatic via the browser's EventSource |
| Broadcast | connection manager iterating a registry, pruning failures |
| Cross-process broadcast | Redis Pub/Sub backplane (module 08) |
| Heartbeat | WebSocket ping/pong; SSE : keepalive\n\n comments |
A complete WebSocket chat with correct cleanup — ws.py:
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active: set[WebSocket] = set()
async def connect(self, ws: WebSocket):
await ws.accept()
self.active.add(ws)
def disconnect(self, ws: WebSocket):
self.active.discard(ws) # idempotent: safe to call twice
async def broadcast(self, msg: str):
for ws in list(self.active):
try:
await ws.send_text(msg)
except Exception:
self.disconnect(ws) # prune sockets that error on send
manager = ConnectionManager()
@app.websocket("/ws/chat")
async def chat(ws: WebSocket):
await manager.connect(ws)
try:
while True:
text = await ws.receive_text() # raises on disconnect
await manager.broadcast(f"user: {text}")
except WebSocketDisconnect:
pass # normal client close
finally:
manager.disconnect(ws) # <-- ALWAYS unregister
await manager.broadcast("a user left")
An SSE progress stream (server→client only) — pushing task progress:
import asyncio, json
from fastapi.responses import StreamingResponse
@app.get("/tasks/{task_id}/stream")
async def stream_progress(task_id: str):
async def gen():
try:
while True:
state = get_task_state(task_id) # e.g. from the result backend
yield f"data: {json.dumps(state)}\n\n"
if state["status"] in ("SUCCESS", "FAILURE"):
return # end the stream
yield ": keepalive\n\n" # comment: keeps proxies happy
await asyncio.sleep(1)
except asyncio.CancelledError:
# client disconnected -> generator is cancelled; clean up here
raise
return StreamingResponse(gen(), media_type="text/event-stream")
Browser side (for reference):
// SSE — automatic reconnection, server->client only
const es = new EventSource("/tasks/abc/stream");
es.onmessage = (e) => console.log("progress", JSON.parse(e.data));
// WebSocket — bidirectional, you handle reconnection yourself
const ws = new WebSocket("wss://example.com/ws/chat");
ws.onmessage = (e) => console.log("msg", e.data);
ws.onopen = () => ws.send("hello");
Hands-on exercises
Continue in bg-queues. pip install "uvicorn[standard]" (WebSocket
support). Use websocat or a couple of browser tabs to connect.
1. A first WebSocket echo
Write a /ws/echo endpoint that accepts, then loops receiving text and
sending it back. Connect with websocat ws://localhost:8000/ws/echo and type.
Expected: the server echoes each line back over the same connection — no new
request per message. That persistent, bidirectional channel is the WebSocket.
2. Broadcast chat to multiple clients
Implement the ConnectionManager chat above. Open three websocat
connections (or browser tabs). Send from one. Expected: all three receive the
message — the server pushed to every registered connection. This is what a
registry buys you.
3. Watch the registry grow and shrink
Add a /count HTTP endpoint returning len(manager.active). Connect and
disconnect clients while polling /count. Expected: the count rises on
connect and falls on clean disconnect (Ctrl+C the client) — because
finally unregisters. Keep this endpoint; it's your leak detector.
4. Diagnose and fix: the connection leak
Break the manager by removing the finally and only unregistering inside a
non-disconnect branch:
@app.websocket("/ws/leaky")
async def leaky(ws: WebSocket):
await manager.connect(ws)
while True:
try:
text = await ws.receive_text()
await manager.broadcast(text)
except WebSocketDisconnect:
break # leaves the loop but NEVER calls disconnect()
Connect several clients and kill them (close the tab / kill websocat),
watching /count. Expected: the count only ever goes up — dead connections
are never removed. Now every broadcast also iterates dead sockets. This is
the leak. Fix it by moving manager.disconnect(ws) into a finally so it
runs on every exit path, clean or not. Re-run and confirm /count drops
when clients vanish.
5. Prune on send failure too
Even with finally, a client can die between receives (so the server is
blocked on receive_text and hasn't noticed). Confirm that broadcast's
try/except prunes such a socket the next time you broadcast to it (the
send_text raises). Expected: a client killed mid-connection is removed on the
next broadcast, not left forever. Two cleanup paths — finally and
send-failure pruning — cover the cases each other misses.
6. Build an SSE progress stream instead
Implement /tasks/{id}/stream from the reference, backed by a Celery task
whose progress you can read (use self.update_state(state=..., meta=...) in
the task). Consume it with curl -N http://localhost:8000/tasks/abc/stream.
Expected: a stream of data: {...} lines, one per second, ending when the task
finishes. Note you did this with plain HTTP and no connection manager —
server→client only.
7. SSE auto-reconnect vs. WebSocket manual reconnect
In a tiny HTML page, open the SSE stream with EventSource and the chat with
WebSocket. Kill and restart the server. Expected: the EventSource
reconnects on its own; the WebSocket does not (its onclose fires and
stays closed until you write reconnection logic). Lesson: SSE's built-in
reconnection is a real ergonomic win when you only need server→client.
8. Choose the right tool for three features
For each, decide polling / SSE / WebSocket and justify: (a) a live "your export is N% done" progress bar; (b) a two-player tic-tac-toe game; (c) a "new blog comments" badge that can be a few minutes stale. Expected reasoning: (a) SSE — server pushes, client only listens; (b) WebSocket — both players send moves, low latency; (c) polling — infrequent, staleness tolerable. Write your justifications before checking the quiz.
9. Heartbeat to detect a silently-dead client
Add a background ping loop to the chat that sends a ping every 20s and closes/
unregisters a connection that fails. Simulate a "network drop" by pausing a
client without closing it cleanly. Expected: within a ping cycle the server
detects the dead socket and removes it from active, rather than holding it
forever. Lesson: heartbeats catch the disconnects the OS never reported.
10. Read the handshake by hand
With the echo server running, send the upgrade request yourself:
curl -i -N --http1.1 -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" \
http://localhost:8000/ws/echo
Expected: HTTP/1.1 101 Switching Protocols with a Sec-WebSocket-Accept
header, then curl hangs on the open connection (Ctrl-C). Repeat without the
Sec-WebSocket-Version header: the server rejects the upgrade with an
ordinary HTTP error (typically 400, or 426 Upgrade Required from some
servers), not 101. Lesson: before the first frame, WebSocket is just HTTP.
11. Diagnose and fix: the proxy idle-timeout drop
Put nginx in front of the app using the snippet from Concepts, but with
proxy_read_timeout 5s and your heartbeat loop from exercise 9 turned off.
Connect through the proxy with websocat and send nothing for 10 seconds.
Expected: the connection closes even though client and server are both
healthy. The proxy treated a quiet long-lived connection like a stalled HTTP
request. Fix: raise proxy_read_timeout above your heartbeat interval,
turn the heartbeat back on so the connection is never idle from the proxy's
point of view, and confirm it survives the same quiet period. Also try removing
the two proxy_set_header lines: the handshake then fails (no 101), which
is the other common proxy bug.
Independent challenge
No code given. Build a real-time "job status" feature: a client connects and subscribes to updates for a specific background job id, and as the job progresses (queued → running → done/failed) the server pushes each state change to that client the instant it happens. Decide SSE vs. WebSocket and justify it (hint: does the client need to send anything over the channel?). Guarantee that when a client disconnects — cleanly or by vanishing — its connection/stream is fully cleaned up with no leak, and prove it with a live connection-count endpoint that returns to zero after all clients leave. Include a heartbeat so a silently-dead client is detected and cleaned up within a bounded time.
The connection-count-returns-to-zero proof is the same leak-detection technique from exercise 3-4; the "push job progress" data can come from the task-state mechanism you used to poll in 00-task-queues-fundamentals, now pushed instead of polled.
Hint
Since the client only listens to job progress and never sends over the
channel, SSE is the better fit — you get automatic reconnection for free and
skip the connection-manager bookkeeping. Cleanup for SSE lives in the
generator: when the client disconnects, the async generator is cancelled
(asyncio.CancelledError) — decrement your active-stream counter in a
finally. If you do choose WebSockets, unregister in a finally and prune on
send failure, exactly as in the chat example.
Common mistakes & troubleshooting
- The connection leak: not unregistering on every exit path. Removing a
connection only in the happy branch (or forgetting
finally) leaves dead connections in your registry forever, growing memory and slowing every broadcast until the server falls over. Always unregister infinallyand prune on send failure. - Using WebSockets when SSE would do. If the client only listens (progress, notifications, feeds), SSE is simpler, runs over plain HTTP, and reconnects automatically. Reserve WebSockets for genuinely bidirectional needs.
- Assuming clients disconnect cleanly. Tabs close, networks drop, laptops sleep — you often get no clean close. Detect via exceptions on send/receive and heartbeats, and clean up regardless.
- Broadcasting from one process and expecting all clients to hear it. A connection manager only reaches connections on its own process; multi-process deployments need a Redis Pub/Sub backplane (module 08).
- Mutating the connection set while iterating it. Iterate a copy
(
list(self.active)) so pruning failed sockets mid-broadcast doesn't raise. - No heartbeat. Silently-dead connections linger and proxies time out idle streams. Send periodic pings (WS) / keepalive comments (SSE).
- A proxy in front that isn't WebSocket-aware. If the proxy doesn't
forward
Upgrade/Connection, the handshake never returns101; if its read timeout is shorter than your heartbeat interval, connections "randomly" drop after N seconds of quiet. Configure both (see Concepts). - Doing heavy work inside the WebSocket receive loop. Blocking the loop delays all messages on that connection; offload slow work to a task (module 00) and push the result when done.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- Give the decision rule for polling vs. SSE vs. WebSockets in one line each.
- What is the one capability WebSockets have that SSE does not, and what does SSE give you "for free" that WebSockets don't?
- Describe the WebSocket connection leak precisely: what accumulates, why, and what's the consequence under real traffic?
- What is the single most important structural fix that prevents the leak, and
why must it be in a
finallyblock specifically? - Why does a connection manager in one process fail to broadcast to all users in a multi-process deployment, and what fixes it?
- Why do you need heartbeats even if you correctly unregister on disconnect?
- For a "your export is 40% done" progress bar, which of the three would you choose and why?
Answers
- Polling: infrequent updates where a few seconds of lag is acceptable and you want maximum simplicity. SSE: the server pushes and the client only listens (notifications, progress, feeds). WebSockets: both sides send, interactive, low latency (chat, games, collaboration).
- WebSockets are full-duplex — the client can send over the persistent channel
too; SSE is server→client only. SSE gives automatic reconnection (via the
browser's
EventSource) and runs over plain HTTP, which WebSockets don't provide out of the box. - Connections that were added to the broadcast registry on connect but never removed on disconnect accumulate in the registry. It happens because cleanup was only on the happy path / not guaranteed, and clients often vanish uncleanly. Under real traffic (constant connect/disconnect churn) the registry grows without bound, wasting memory and making every broadcast iterate dead sockets, until the server exhausts resources.
- Unregistering the connection in a
finallyblock so it runs on every exit path — normal close,WebSocketDisconnect, or any other exception. It must befinallybecause you cannot predict how the connection ends; onlyfinallyguarantees the cleanup runs regardless of which branch/exception terminated the handler. - Each process's manager only knows about connections made to that process, and a client is connected to just one process; a message produced on one process can't reach clients on the others. A shared pub/sub backplane (Redis Pub/Sub) that every process publishes to and subscribes from fixes it (module 08).
- Because a client can die without any clean close or OS notification (yanked network, slept laptop); the connection looks open but is dead, so you never hit the disconnect path. Heartbeats detect the silent death (missed pings) so you can close and unregister it, and keep proxies from timing out idle connections.
- SSE — the server pushes progress updates and the client only listens (nothing needs to be sent from client to server over the channel), so SSE's simplicity, plain-HTTP transport, and automatic reconnection make it the better fit than WebSockets, and it's more timely than polling.
Cumulative review
Closed-book. Don't reopen modules 00-07 while attempting these.
- A
POST /videosendpoint accepts an upload, transcodes it (slow, CPU- heavy), emails the user when done, and fires a webhook to a partner. Design the whole flow: what returns immediately, what runs in the background and how the steps are wired, how the partner webhook is emitted without risking a fire-for-a-rolled-back-event, and how the user sees live progress. Name the module concepts for each piece. - Your transcoding task is enqueued but "sometimes runs twice and produces two output files." Give the delivery-model reason it can run twice and the fix that makes the effect happen once.
- A webhook you receive credits an account. Walk through every defense that stops (a) a forged request, (b) a replayed request, and (c) a legitimately- retried duplicate from each causing a wrong credit.
- You added two Celery Beat replicas for availability and now a nightly job runs twice. Explain the cause and give the two-layer fix.
- Your WebSocket server's memory climbs steadily over days and broadcasts get slower. Name the bug, its root cause in code, and the exact fix.
- A teammate built "live notifications" with WebSockets and is fighting reconnection logic and proxy issues. What would you switch to and what two problems does that immediately solve?
- A transactional receipt email lands in customers' spam even though your send code is correct and works against MailHog. List the non-code layer you'd investigate and name what SPF, DKIM, and DMARC each do.
- You need to deliver a webhook to five subscribed endpoints, one of which is frequently down. Describe the delivery design that keeps the healthy four fast and reliable while the fifth retries, and how retries stay deduplicatable by the receiver.
- Explain why sending a webhook, sending an email, and processing a received webhook should all happen in background tasks rather than inline — and what specific bad thing happens inline in each case.
Show answers
POST /videosvalidates, stores the file, writes the "video.uploaded" state and an outbox row, enqueues the pipeline, and returns202with a job id immediately (module 00). The background work is a chain:transcode.s(id) | notify_email.si(id) | emit_partner_event.si(id)— chain so a transcode failure stops the rest (module 01), transcode retrying transient errors idempotently (module 02). The partner webhook is emitted via a transactional outbox written in the same txn as the "done" state and relayed after commit, so a rollback never fires it (module 05), delivered with backoff retries and a stable event id (module 05/06). The user sees progress via an SSE stream of task state (module 07). The email is a transactional send from a task with retries + idempotency (module 04).- At-least-once delivery: a worker can complete the transcode then crash before acking, so the broker redelivers and it runs again (module 00/02). Fix: make it idempotent — derive a deterministic output key from the video id and skip/overwrite if that output already exists (check-before-act with a stable key), so two runs produce one output.
- (a) Forged: HMAC signature verification over the raw bytes with the shared
secret, constant-time compared — no secret, no valid signature,
401. (b) Replayed: a signed timestamp checked against a freshness window rejects a stale captured request; and idempotency by event id makes any within-window replay a no-op. (c) Duplicate retry: the atomic per-event-id dedupe record means the second delivery returns2xxand skips the credit; the effect happens once. All three: process in a task after a fast2xx. - Both Beat replicas independently reach each due entry and enqueue it, so the job runs twice. Fix: run exactly one Beat instance (availability via fast restart), and guard the task with a distributed lock keyed by job + time window and/or make its effect idempotent so a duplicate fire no-ops.
- A WebSocket connection leak: connections are added to the broadcast registry
on connect but not removed on every disconnect path (missing/incorrect
finally), so dead connections accumulate — climbing memory and slower broadcasts (each iterates dead sockets). Fix: unregister in afinallyblock and prune sockets that error on send. - Switch to SSE. It immediately solves reconnection (the browser's
EventSourcereconnects automatically) and proxy/infrastructure friction (it's plain HTTP), since notifications are server→client-only and don't need the client to send over the channel. - The deliverability/DNS + reputation layer, not the code. SPF: a DNS record listing which servers may send for your domain. DKIM: a cryptographic signature (public key in DNS) proving the message came from your domain unaltered. DMARC: a DNS policy telling receivers what to do when SPF/DKIM fail, plus reporting. Also check bounce/complaint suppression and whether you send a text part.
- Fan out to one delivery task per (event, endpoint) so each retries independently — the healthy four deliver immediately while the fifth backs off and retries over a long window without blocking the others. Every retry of the fifth reuses the same event id, so when it finally lands the receiver can dedupe it.
- All three are slow, I/O-bound, third-party-dependent work that shouldn't block the request-response cycle. Sending a webhook inline: a slow/dead consumer blocks your request/transaction — their outage becomes yours. Sending email inline: the user waits on your mail provider's latency. Processing a received webhook inline: you blow the sender's timeout, so it marks the delivery failed and retries, amplifying load and duplicates.
Further reading & sources
- MDN: WebSockets API - the client protocol, lifecycle, and framing.
- MDN: Using server-sent events -
EventSource, thetext/event-streamformat, and automatic reconnection. - FastAPI: WebSockets - accepting connections, the receive loop, and
WebSocketDisconnect. - MDN: WebSockets vs Server-sent events - when one-way SSE is the simpler, more robust choice.
- RFC 6455: The WebSocket Protocol - the handshake, framing, and ping/pong heartbeats.
Free PDF notes
Download the notes for each video on this page as a printable PDF, with every diagram and interview questions: SD-B-014: WebSockets, SD-B-015: SSE vs Long Polling vs WebSockets, SD-B-016: HTTP Streaming. Each section in one file: Phase 2, Networking (PDF).
Next
08-pub-sub-architecture — module 07 left a gap: a connection manager can only broadcast to clients on its own process. Next you'll close it with the publish/subscribe pattern — using Redis Pub/Sub as a backplane so a message from any process reaches connections on every process — and learn the crucial difference between a pub/sub bus (fire- and-forget, lost if nobody's listening) and a durable task queue.