Design Facebook Live Comments
Real-Time Comment Fan-out System Design
Overview
A celebrity goes live. Within seconds a million people are watching the same video, and a river of comments scrolls up the side of every viewer's screen — "🔥", "hi from Brazil", "first!" — appearing within a second or two of being typed. That comment stream is the feature we're designing. Anyone watching can comment; everyone watching sees comments flow by in near real time; and the whole thing is ephemeral — nobody scrolls back through the comments on a two-hour livestream the way they'd scroll a chat history.
It is labeled "Hard" because it inverts the shape of a normal chat problem. In WhatsApp the unit is a private conversation with one recipient; in Slack a channel has maybe tens of thousands of members. Here one live video is a single logical channel with millions of concurrent subscribers, and that one number breaks the usual playbook. No single server can hold a million connections or push to a million clients. The write side spikes too — when something dramatic happens on stream, tens of thousands of people comment in the same second. And at the extreme, the read side hits a wall that has nothing to do with servers: a human cannot read a million comments per second, so past a certain rate you physically cannot — and shouldn't try to — show every comment to every viewer. You sample.
Those three pressures define the whole design. In this walkthrough you'll scope it, size it, model the (deliberately thin) data, design the client contract, draw the ingest and fan-out paths, and then go deep on exactly three things: fanning out to millions on one channel, absorbing the ingest spike while sampling the read side, and managing millions of connections across a geo-distributed fleet.
Out of scope (say so): the video encoding and delivery pipeline itself (that's a live video streaming problem — comments ride alongside it, but we don't design the transcoder or the CDN for the media); the recommendation/ranking model that decides which live videos to surface; and deep abuse-detection ML (we note where moderation hooks in). We keep real-time comment ingest, fan-out to millions on one channel, sampling at extreme scale, and connection management firmly in scope — they are the whole problem.
Functional requirements
- Post a comment. Any viewer of a live video can post a short text comment; it enters the stream for that video.
- See comments in real time. A viewer sees a live-updating stream of comments on the video they are watching, appearing within a second or two of being posted.
- Join mid-stream and see recent context. A viewer who opens the video 40 minutes in immediately sees the last few comments, not a blank feed, then the live flow continues.
- Ordering that reads naturally. Comments appear in an order that feels right (roughly the order they were sent); we do not need a single globally exact total order.
Out of scope (state it, so you don't burn time on it): the video/audio stream itself, likes/reactions on the whole video (touched in Extensions), durable comment archives you can scroll years later, and threaded replies to a comment. A Live comment stream is ephemeral and flat — that constraint is a gift, and leaning on it is part of a strong answer.
Non-functional requirements
- Real-time, low-latency fan-out. A comment should reach watching viewers within ~1–2 seconds — target < 2 s p99. It's a live event; a comment that lands 20 seconds late is worthless.
- Extreme fan-out on a single channel. One hot video can have millions of concurrent viewers, all subscribed to the same comment stream. The design must fan one comment out to millions without any single server owning that channel.
- Absorb write spikes. Comment posting is bursty — a dramatic on-stream moment produces a sudden spike of tens of thousands of comments/sec on one video. Ingest must not fall over or block posters.
- Graceful degradation over completeness. At extreme scale it is acceptable — necessary — to drop or sample comments rather than deliver every one to every viewer. A viewer seeing a representative, readable stream is the goal, not a guarantee that they saw comment #4,812,003.
- Availability over strong consistency. A slightly-out-of-order or lightly-sampled stream is fine; an error page or a frozen feed is not. Comments are ephemeral, so losing some under load costs little.
- Horizontal scale. Connections, comment writes, and fan-out all grow with viewers, so every layer scales by adding machines, not by buying a bigger one.
Estimations
State assumptions; the goal is to justify the connection layer, the tiered fan-out, and the sampling decision — not to be exact.
Rounded assumptions
- One hot live event: ~2M concurrent viewers. This is the number that breaks everything, so we design for it. Most live videos are tiny (dozens to thousands); the system must span both.
- Each viewer holds one live connection to receive comments → ~2M concurrent connections for one video, tens of millions across all concurrent live videos platform-wide.
- Comment posting rate at peak on the hot video: assume ~1% of viewers comment in a hot 10-second window →
2M × 1% / 10s ≈ ~2,000 comments/sec, spiking to ~10,000+ comments/sec on a dramatic moment. Call it ~10K writes/sec on one video at peak. - Fan-out amplification is the killer number. If we naively delivered every comment to every viewer:
10K comments/sec × 2M viewers = 2×10^10 = 20 billion deliveries/secfor one video. That is absurd — and it's the whole reason for both tiered fan-out and sampling. - Gateway count (justifies the registry being small). At ~100K connections per gateway, one hot video's
2M viewers ÷ 100K ≈ ~20 gateways. So thevideo → gatewaysregistry entry for even the hottest video is only ~20 entries — the fan-out service does ~20 publishes per comment, not millions. Fleet-wide, say ~100K concurrent live videos at ~200 avg viewers ≈ ~20M connections total ≈ ~200 gateways across the fleet — small enough to registry-map cheaply. - Sampling target: a human can read maybe ~10–20 comments/sec before the stream is an unreadable blur. So we cap the shared per-video stream at ~10–20 comments/sec kept and published once — every local viewer receives the same sampled bytes. On the hot video that's
2M viewers × ~15 displayed/sec = 3×10^7 = ~30M deliveries/secat the edge — still huge, but ~600× smaller than delivering everything, and now bounded by what humans can read rather than by how fast people type.
How the numbers affect the design
| Signal from the numbers | Design decision |
|---|---|
| ~2M viewers on one video, all on one comment stream | One video = one hot channel; no single server can own it — fan out hierarchically. |
| ~2M+ connections per hot video, tens of millions total | A fleet of geo-distributed gateway servers, each holding ~100K–1M connections; a connection registry. |
| ~10K comment writes/sec spiking on one video | A write path that buffers and absorbs bursts (a log/queue), decoupled from fan-out. |
| Naive delivery = ~20B deliveries/sec | Impossible and pointless — sample/rate-limit the displayed stream to what a human can read. |
| Comments are ephemeral, no deep scrollback | Store only a short recent window; the stream is a rolling buffer, not an archive. |
Storage
- Per comment: id (~16 bytes), video id (~16 bytes), author id (8 bytes), timestamp (8 bytes), body (~80 bytes avg — Live comments are short) ≈ ~130 bytes. Round to ~150 bytes with overhead.
- We do not durably store every comment forever. We keep a rolling recent window per video (say the last few hundred to few thousand comments, plus a short TTL buffer for mid-stream joiners). Even at 10K/sec, a 60-second window is
10K × 60 × 150 bytes ≈ ~90 MBper hot video — trivially held in an in-memory ring buffer / cache. Optionally spool a sampled subset to cheap storage for post-hoc VOD replay (an Extension), but that is not the live path.
Caution
Do not model this like a durable chat log partitioned for infinite scrollback. Live comments are ephemeral and flat: you need the last N comments in memory for real-time fan-out and mid-stream joins, not petabytes of history. Reaching for a Cassandra-scale durable store here is over-engineering the wrong axis — the hard axis is fan-out width and write-spike absorption, not durable storage.
Core entities / data model
The data model is deliberately thin — leaning on ephemerality is half the design.
| Entity | Key fields |
|---|---|
LiveVideo |
video_id (PK), author_id, status (live/ended), started_at, viewer_count |
Comment |
comment_id, video_id, author_id, seq, timestamp, body |
RecentWindow |
per video_id → an in-memory rolling buffer of the last N comments (for fan-out + mid-stream join) |
ConnectionEntry |
(video_id, gateway_id) → this gateway holds some connected viewers of this video (fan-out routing) |
The heavy structure is not a table — it's the RecentWindow, an in-memory ring buffer of recent comments per live video, held close to the fan-out layer (e.g. Redis or in gateway-local memory). It answers the only two reads that matter on the live path: "what should I push next?" and "a viewer just joined 40 minutes in — give them the last ~50 comments." When the video ends, the window is discarded (or a sampled subset is spooled off for VOD).
Each comment gets a seq, a per-video sequence number that gives an approximate order (Deep Dive 2 explains why approximate is not only acceptable but the right call). The primary mechanism is to use the per-video log's append offset as seq — you get ordering for free with no extra coordination. If you need a dense, human-facing sequence, you can layer an explicit per-video counter on top. It is not a globally exact, gap-free total order — chasing that would be a coordination bottleneck for zero user benefit. If the log is partitioned for write throughput, ordering is per-partition, which is fine for approximate ordering.
Note
Separate the ephemeral live path (the recent-window ring buffer feeding real-time fan-out) from any durable/VOD path (an optional sampled archive written asynchronously). The first is what makes "millions see comments live" work; the second is a nice-to-have that must never be on the hot path. Conflating them pushes you toward a durable store you don't need and can't fan out from fast enough.
API design
Live comments are not a plain REST API. Posting a comment is an ordinary request, but receiving the stream can't be — a viewer doesn't know when the next comment will arrive, so it can't poll for each one at the right moment. Instead the client opens one long-lived connection and the server pushes comments down it. We describe that channel explicitly alongside the REST endpoint.
Real-time channel (persistent connection)
Client opens a WebSocket: wss://live.example.com/connect?video_id=...&token=...
authenticates, subscribes to ONE video's comment stream, keeps the connection OPEN
Client to server events:
{ "type": "comment", "client_msg_id": "<uuid>", "video_id": "...", "body": "..." }
{ "type": "ping" } heartbeat to keep the connection alive
Server to client events (pushed, no request):
{ "type": "comment", "video_id": "...", "seq": 88213, "author": "...", "body": "..." }
{ "type": "backfill", "video_id": "...", "comments": [ ... last ~50 ... ] }
{ "type": "ack", "client_msg_id": "<uuid>", "comment_id": "...", "seq": 88213 }
The client opens one WebSocket when it opens the video and keeps it open for as long as it's watching. Crucially, a connection subscribes to exactly one video's comment stream — that's the whole point of the hot-channel model. On connect the server sends a backfill event with the recent window so a mid-stream joiner isn't staring at a blank feed, then streams comment events as they're fanned out. A periodic ping heartbeat keeps the connection alive and lets the gateway detect a dead one within seconds.
Note
Recommendation: use WebSocket. Server-Sent Events (SSE) is the leaner fit on paper — comment delivery is overwhelmingly one-directional (server → viewer), and SSE gives you server push over ordinary HTTP with simpler infra and automatic reconnect. But committing to WebSocket lets a viewer post over the same connection they receive on, which simplifies the client contract (one socket, one auth, one reconnect path) even though the live stream is one-way. That single-connection simplicity is worth more here than SSE's infra savings, so we standardize on WebSocket. Either way the key property is the same: the server pushes, the client does not poll.
Post a comment (HTTP fallback for the WebSocket comment)
POST /api/videos/{video_id}/comments
Body: { "client_msg_id": "<uuid>", "body": "..." }
Returns: 202 { "comment_id": "...", "seq": 88213 }
Post a comment into a video's stream. The client_msg_id is a client-generated idempotency key: if the network drops and the client resends, the server recognizes the same id and returns the original result instead of double-posting. It returns 202 because the comment is accepted into the ingest pipeline and fanned out asynchronously — the poster doesn't block on delivery to 2M people.
Join / get recent window (on connect)
GET /api/videos/{video_id}/comments/recent?limit=50
Returns: 200 { "comments": [ ... last 50, oldest-first ... ], "cursor_seq": 88213 }
Fetch the recent-window snapshot for a mid-stream joiner (also delivered inline as the backfill event over the socket). It reads the in-memory ring buffer, never a durable scan. There is deliberately no deep-history pagination endpoint — the stream is ephemeral.
High-level architecture
Three flows matter: ingesting a comment and absorbing the write spike, fanning one comment out to millions of viewers on one channel, and a viewer joining mid-stream. The shared cast of components:
- Gateway servers (the connection layer). Each holds a large number of long-lived viewer connections (e.g. ~100K–1M per server). Their only job is to keep connections open, subscribe to the videos their viewers are watching, and push comments down. They are deliberately dumb pipes; the fleet is geo-distributed so viewers connect to a nearby edge.
- Ingest service + comment log. Accepts posted comments, assigns a
seq, dedups, and writes them onto a per-video log/queue (Kafka-style topic per hot video) that absorbs bursts and decouples writers from fan-out. - Fan-out / dispatch service. Reads the per-video comment log, applies sampling/rate-limiting, and publishes the surviving comments to the fan-out fabric that reaches gateways.
- Recent-window store. An in-memory ring buffer per video (Redis or gateway-local) holding the last N comments for real-time push and mid-stream backfill.
- Connection registry. A fast lookup mapping each live video to the set of gateways currently holding viewers of it (
video → {gateways}), so a comment is published only to gateways that actually have viewers — not the whole fleet.
1. Ingest path (absorbing the write spike)
A viewer posts a comment; the write side may be spiking to tens of thousands/sec on this one video.
flowchart LR
Poster[Poster's client] -->|"comment (WebSocket or POST)"| GW[Gateway]
GW -->|"forward"| Ingest[Ingest Service]
Ingest -->|"dedup by client_msg_id + assign seq"| Ingest
Ingest -->|"append"| Log[[Per-video Comment Log]]
Ingest -.->|"202 accepted"| Poster
Log --> Fan[Fan-out Service]
- The poster's client sends the comment over its WebSocket (or a plain HTTP POST) to a gateway.
- The gateway hands it to the ingest service, which dedups on
client_msg_id, assigns the video's nextseq, and appends it to the per-video comment log — a durable-enough buffer that absorbs the burst. - Ingest immediately acks the poster (
202). The poster does not wait for the comment to reach 2M viewers; fan-out happens downstream and asynchronously. - The comment now sits on the log, ready for the fan-out service to consume, sample, and dispatch.
Note
The per-video log is the shock absorber. Writers append at whatever spiky rate they want; the fan-out side consumes at a controlled rate. This decoupling is what lets a dramatic on-stream moment produce 10K comments/sec without knocking over delivery — the burst lands in the log, not directly on 2M sockets.
2. Fan-out path (one comment to millions)
The interesting case: one comment must reach ~2M viewers spread across many gateways — and we can't and won't deliver every comment to every viewer.
flowchart LR
Log[[Per-video Comment Log]] --> Fan[Fan-out Service]
Fan -->|"sample / rate-limit<br/>to human-readable rate"| Fan
Fan -->|"publish once to<br/>video topic"| Topic{{Video Pub/Sub Topic}}
Topic --> GWA[Gateway A]
Topic --> GWB[Gateway B]
Topic --> GWC[Gateway C]
GWA -->|"local fan-out to<br/>its viewers"| VA[Viewers on A]
GWB -->|"local fan-out"| VB[Viewers on B]
GWC -->|"local fan-out"| VC[Viewers on C]
- The fan-out service consumes the per-video log and applies sampling / rate-limiting — capping the outbound stream to the ~10–20 comments/sec a human can actually read (Deep Dive 2).
- It publishes each surviving comment once to the video's pub/sub topic — not once per viewer.
- Every gateway holding viewers of that video is subscribed to the topic (via the connection registry). Each receives the comment once.
- Each gateway does the last-hop local fan-out: it pushes the comment down the sockets of its viewers of that video. The million-way amplification happens at the edge, spread across the whole gateway fleet — never in one place.
Tip
The mental model: the fan-out service does O(gateways) work per comment (one publish, received by maybe a few hundred gateways), and each gateway does O(its local viewers) work. Nobody does O(all 2M viewers). That split — coarse fan-out to gateways, fine fan-out at the edge — is the heart of the whole design.
3. Mid-stream join path
A viewer opens the video 40 minutes in and must not see a blank feed.
flowchart LR
Viewer[New viewer] -->|"connect + subscribe video"| GW[Gateway]
GW -->|"register: this gateway<br/>now has a viewer of video"| Reg[(Connection Registry)]
GW -->|"fetch recent window"| RW[(Recent-Window Buffer)]
RW -->|"last ~50 comments"| GW
GW -->|"backfill event, then live flow"| Viewer
- The new viewer connects to a nearby gateway and subscribes to the one video.
- The gateway registers
(video → this gateway)in the connection registry — so future comments for this video get published to it — and, if it's the first viewer of this video on this gateway, subscribes to the video's pub/sub topic. - The gateway reads the recent-window buffer for the video and sends the last ~50 comments as a
backfillevent, so the viewer immediately sees context. - From then on the viewer receives the live flow like everyone else.
4. Combined architecture
flowchart LR
Poster[Poster] --> GWX[Gateway X]
Viewer[Viewers] --> GWY[Gateway Y]
GWX -->|"post"| Ingest[Ingest Service]
Ingest -->|"seq + dedup"| Log[[Per-video Comment Log]]
Log --> Fan[Fan-out Service]
Fan -->|"sample + publish once"| Topic{{Video Pub/Sub Topic}}
Fan -->|"append recent"| RW[(Recent-Window Buffer)]
Topic -->|"deliver to gateways<br/>with viewers"| GWY
GWY -->|"local push"| Viewer
GWY -->|"register/deregister"| Reg[(Connection Registry<br/>video to gateways)]
Reg -->|"video→gateways lookup"| Fan
RW -.->|"mid-stream backfill"| GWY
The fan-out service is the hub of the live path: it consumes the per-video log, samples it down to a human-readable rate, appends survivors to the recent-window buffer (for joiners), and publishes them once to the video topic. Gateways subscribe per video and do the edge fan-out. The connection registry keeps video → gateways fresh so publishes only touch gateways that actually have viewers.
Tip
Lead with a small live video (a few hundred viewers, direct fan-out) so the interviewer sees the happy path, then introduce the 2M-viewer hot video that forces tiered fan-out and sampling. Jumping straight to sampling before establishing the basic push model reads as skipping steps.
Deep dives
1. Fan-out to millions on one channel (the hot-channel problem)
This is the defining problem. One live video is a single logical channel with up to millions of subscribers, and one comment must reach all of them within a second or two. The naive approaches fail in instructive ways.
Caution
The single most common mistake is treating this like per-recipient chat fan-out. In a DM or small group you look up each recipient and forward to their connection server — O(recipients) targeted sends. Do that here and one comment on the hot video becomes ~2M individual lookups-and-forwards. At 10K comments/sec that's ~20 billion sends/sec. Per-user fan-out is correct for a DM and catastrophic for a hot channel. Say why it doesn't transfer.
The second trap is subtler: letting one server own the channel.
One server owns the video's channel and fans out to all viewers (avoid)
A single "comment server" for the video holds (or reaches) all 2M viewer connections and pushes each comment to all of them itself.
The consequence: no single machine can hold 2M live connections (memory, file descriptors, NIC throughput) or push millions of messages per comment. It's a single point of failure — if it dies, every viewer of the hottest video on the platform drops at once — and it cannot be scaled by adding machines because the channel is indivisible on one host.
- Pro: conceptually simple; one place holds the channel state.
- Con: physically impossible at 2M viewers; single point of failure; unscalable.
- Verdict: avoid. The channel must be spread across many servers, which is exactly what tiered fan-out does.
Hierarchical / tiered fan-out — pub/sub to gateways, edge fan-out to viewers (recommended)
Split the fan-out into two tiers. The fan-out service publishes each comment once to the video's pub/sub topic. Every gateway that holds at least one viewer of the video is subscribed to that topic (recorded in the connection registry when its first viewer of the video joins). Each gateway receives the comment once and does the last-hop edge fan-out to its own local viewers of that video.
flowchart TB
Fan[Fan-out Service] -->|"publish once"| Topic{{Video Topic}}
Topic --> GW1[Gateway 1<br/>holds 300K viewers]
Topic --> GW2[Gateway 2<br/>holds 300K viewers]
Topic --> GWn[Gateway N<br/>holds 300K viewers]
GW1 --> V1[local push<br/>to 300K sockets]
GW2 --> V2[local push<br/>to 300K sockets]
GWn --> Vn[local push<br/>to 300K sockets]
style Fan fill:#dcfce7,stroke:#16a34a,color:#14532d
For 2M viewers at ~300K per gateway, that's ~7 gateways subscribed to the topic. The fan-out service does ~7 publishes per comment; each gateway does 300K local pushes. The wide amplification is aggregated at the edge — spread across the fleet, parallelized, and each gateway only ever touches its own connections.
- Pro: central work is O(gateways) — tiny; the O(viewers) work is distributed across the whole fleet and parallel. Add viewers → add gateways; the topic-publish cost barely moves. No single owner, no single point of failure.
- Con: more moving parts (a pub/sub fabric, per-video topics, a registry of which gateways hold which video). A very hot video still concentrates many gateways on one topic, so the topic/bus itself must scale (partition the topic, or add a middle relay tier for truly enormous events).
- Verdict: the standard approach for one-to-millions fan-out. Coarse fan-out to gateways, fine fan-out at the edge.
Why not per-user fan-out (restated crisply). In chat, per-user fan-out wins because recipients are different people in different conversations — there's no shared channel to exploit. Here, 2M viewers share one stream, so you publish once and let the subscription structure do the routing. Turning a shared broadcast into 2M individual sends throws away the one property that makes the problem tractable.
Extremely hot videos. If a single topic's subscriber set or throughput gets too large, add a middle relay tier: the fan-out service publishes to a handful of relay nodes, each relay forwards to a slice of the gateways. That's a third tier — service → relays → gateways → viewers — and it's the same trick applied recursively. You rarely need it, but naming it shows you understand the fan-out is hierarchical, not two fixed levels.
2. Comment ingest spike, ordering, and sampling
Two things spike here: the write rate (many people commenting at once) and the implied read rate (every comment times every viewer). Both need handling, and the read side leads somewhere counterintuitive — you deliberately throw comments away.
Absorbing the write spike. A dramatic moment produces a sudden burst of comments on one video. Instead of processing each synchronously on the post path, ingest appends to a per-video log/queue (a Kafka-style topic) and acks the poster immediately. The log absorbs the burst; the fan-out service consumes it at a controlled rate. Ingest also dedups on client_msg_id (a retried post is idempotent) and assigns the seq.
Ordering: approximate is the right call.
Strict global total order across all commenters (usually unnecessary)
Enforce a single, exact, gap-free order for every comment on the video, agreed by all viewers.
The consequence: with tens of thousands of comments/sec arriving concurrently from all over the world, a strict total order requires funneling every comment through a single sequencer or a consensus round per comment — a coordination bottleneck precisely on the hottest path, for a stream nobody reads that carefully.
- Pro: every viewer sees the exact same order.
- Con: a serialization point per comment at 10K/sec; latency and a scaling ceiling; zero user benefit — humans can't perceive whether two near-simultaneous comments were truly ordered.
- Verdict: usually unnecessary. Reach for it only if the product genuinely requires identical ordering (it almost never does for Live comments).
Approximate ordering by ingest time + per-video seq (recommended)
Order comments by the time they hit the ingest service, stamped with a per-video seq taken from the per-video log's append offset (and, only if you want a dense human-facing sequence, a lightweight per-video counter layered on top). Different viewers may see slightly different micro-orderings of near-simultaneous comments, and that is fine — the stream reads naturally.
- Pro: no global coordination; the log's append order gives a cheap, good-enough order for free; scales horizontally.
- Con: not a strict total order — two comments posted in the same instant may appear in different relative order to different viewers. Nobody notices.
- Verdict: the standard choice. "Roughly the order they were sent" is exactly what a Live comment stream needs.
Sampling: you cannot show every comment to every viewer. This is the insight the question is really probing. At 10K comments/sec, a viewer would receive 10,000 comments per second — an unreadable blur, and 20 billion deliveries/sec across 2M viewers. Since a human reads maybe ~10–20 comments/sec, delivering more is both impossible to sustain and useless to the viewer. So above a rate threshold you sample / rate-limit / prioritize the stream down to a human-readable rate.
flowchart LR
Log[[10K comments/sec]] --> Sampler[Sampler / Rate-limiter]
Sampler -->|"prioritized:<br/>author + friends + ranked"| Keep[~15 comments/sec kept]
Sampler -.->|"dropped from live stream"| Drop[(shed load)]
Keep --> Topic{{Video Topic}}
style Keep fill:#dcfce7,stroke:#16a34a,color:#14532d
How to choose which comments survive:
- Rate-cap per video. Deliver at most ~10–20 comments/sec to the live stream regardless of how many are posted; shed the rest.
- Prioritize, don't just randomly drop. Always include high-value comments: the video author's own comments, verified accounts, and comments ranked highly (engagement, quality/likes on the comment). The sampler keeps one shared prioritized stream and publishes it once — every viewer on a gateway gets the same sampled bytes, which is exactly what makes the dumb edge fan-out cheap.
- Drop low-value first. Spam, duplicates ("first!" × 5,000), and unranked filler are the first to go.
Here is the sampler on the hot video, concretely — a token bucket refilling at ~15 tokens/sec (the human read rate), with priority tiers draining it:
10,000 comments/sec arrive on video V
tier A author + verified -> always emitted (tiny volume, bypass the bucket)
tier B ranked / high-quality -> take tokens until the bucket is near-empty
tier C everything else -> emitted only if tokens remain (almost never, at 10K/sec)
=> ~15 comments/sec published ONCE to V's topic; the other ~9,985 are shed
Note
Personalizing the sample per viewer (weaving in each viewer's own friends' comments) is tempting but not free: it breaks "publish once, dumb edge fan-out," because a gateway can no longer broadcast identical bytes — it needs per-connection selection state. Treat it as a deliberate, more expensive extension: blend the one shared sampled stream with a small per-viewer friend/priority side-stream at the gateway (~doubling edge state), rather than presenting personalization as a costless upgrade.
Warning
Sampling is lossy by design, and that's the whole point — but be explicit about the tradeoff. A comment that gets dropped from the live stream is never seen by most viewers. For an ephemeral, entertainment-oriented Live stream that's the correct trade (readability and survivability over completeness). For something where every comment must be seen — a small Q&A, a paid AMA — you'd disable sampling below a threshold, or fall back to full delivery because the viewer count is small enough. Name which regime you're in.
Note
The rate threshold makes this graceful across scale: a video with 50 viewers and 3 comments/sec is under the cap, so every comment is delivered — no sampling at all. Sampling only kicks in once the rate exceeds what a human can read. The same system serves a tiny stream losslessly and a mega-event by sampling, with one knob.
3. Connection management at scale
Millions of concurrent WebSocket connections for one video, tens of millions across all live videos, spread over a geo-distributed gateway fleet. Holding and maintaining those connections is its own hard problem.
The gateway fleet. Connections terminate on gateway servers, each holding ~100K–1M long-lived connections. They are geo-distributed so a viewer in São Paulo connects to a nearby edge, not across an ocean — hot-channel load is thus spread geographically as well as across machines. A load balancer distributes new connections; because connections are long-lived and cheap-per-message, balance by connection count, not request rate.
Connection registry. A gateway, when its first viewer of a video connects, registers (video → this gateway) and subscribes to the video's topic; when its last viewer of that video leaves, it deregisters and unsubscribes. The fan-out service reads this video → {gateways} map to publish only to gateways that actually hold viewers — never the whole fleet. The registry is small (it's keyed by video and gateway, not by individual user) and lives in a fast store (Redis).
Note
The registry is video → gateways, not user → gateway. For a broadcast channel you never need to route to a specific viewer — only to "every gateway that has viewers of this video." That's far smaller and cheaper than a per-user routing map, and it's another place the shared-channel structure pays off. (Contrast WhatsApp, where a per-user user → server registry is exactly right because delivery is targeted.)
Reconnection and backfill. Connections die constantly — phones lose signal, apps background, gateways get drained. When a viewer reconnects (to any gateway), they get the same treatment as a mid-stream joiner: a backfill of the recent window so they don't miss the last few seconds, then the live flow. Because the stream is ephemeral and sampled, "backfill" means "the last N sampled comments" — at ~15/sec that's only the last few seconds of the readable stream, not "every comment since you dropped." So a viewer who was gone for 30 seconds silently loses the gap in between, and that's fine: the goal is to re-immerse them in the current conversation, not to replay a perfect history.
Where does that recent window live? Two options:
Gateway-local in-memory ring buffer per video (fast, but lost on gateway death)
Each gateway keeps the recent window for its videos in local memory and serves backfill from there.
- Pro: zero-hop, no external read on the hot connect path.
- Con: it vanishes when the gateway dies — which is exactly when a thundering herd of reconnects needs it, so every one of them re-fetches from elsewhere at once.
- Verdict: fine as an L1 cache, insufficient on its own for the failover case.
Redis-backed shared recent-window, cached at the gateway (recommended)
Keep the authoritative recent window per video in a fast shared store (Redis), with each gateway caching it locally.
- Pro: survives gateway death, so a reconnect storm hitting new gateways still gets backfill; it's the same ~N comments for everyone, so one cached read serves the whole herd.
- Con: one more component and a cache-coherence window (backfill can trail the live edge by a comment or two — invisible and acceptable).
- Verdict: the standard call — shared truth for survivability, gateway-local cache for speed.
Graceful failover and draining. When a gateway dies, its viewers reconnect and are spread across the surviving fleet. Two hazards and their mitigations:
- Thundering herd on reconnect. A gateway holding ~1M connections dies and all 1M clients reconnect at once — a thundering herd that hammers the load balancer, registry, and the recent-window store (everyone wants backfill simultaneously). Mitigate with randomized exponential backoff (jitter) on the client so reconnects spread over time, and cache the recent-window aggressively (it's the same ~50 comments for everyone, so one cached read serves the herd).
- Draining, not hard cutover. To take a gateway out for a deploy, stop accepting new connections, let the load balancer remove it, and give existing clients a reconnect window (with jitter) to migrate gradually — rather than killing 1M connections at once and dumping them all on the fleet in one spike.
Backpressure on slow consumers. Each gateway keeps a bounded outbound buffer per connection. On a hot video, a viewer on a weak network can't drain fast enough. Rather than buffer their comments without limit (which would exhaust the gateway's memory and hurt every other viewer on it), apply backpressure — drop the oldest queued comments for that one slow socket (the stream is ephemeral and already sampled, so dropping a few more for one viewer is invisible) or close the socket and let them reconnect for a fresh backfill. One slow consumer must never back up the whole gateway.
Note
Ephemerality makes connection management dramatically easier than in chat. A dropped chat message is a correctness bug you must recover with durable store-and-forward and per-recipient cursors. A dropped Live comment is nothing — the viewer just gets the next one and a recent-window backfill on reconnect. So you can be aggressive: shed load, drop slow consumers, sample hard, and lean on cheap backfill instead of expensive per-viewer delivery guarantees.
Tradeoffs & bottlenecks
- Deliver-every-comment vs sample-at-scale. Delivering every comment is impossible past ~10–20/sec/viewer and useless (unreadable). Sampling down to a human-readable rate — prioritizing author, friends, and ranked comments — is the defining trade. The cost is that dropped comments are never seen by most viewers; for an ephemeral stream that's acceptable, and it's disabled automatically for small streams under the rate cap.
- Direct fan-out vs tiered/edge fan-out. Direct per-viewer fan-out is fine for a small video and fatal for a hot one. Tiered fan-out (publish once to gateways, edge fan-out to viewers) makes central work O(gateways) and distributes the O(viewers) amplification across the fleet — at the cost of a pub/sub fabric, per-video topics, and a registry to maintain.
- Strict vs approximate ordering. A strict global total order is a per-comment serialization bottleneck for zero perceptible benefit. Approximate ordering by ingest time + per-video
seqscales freely and reads naturally. Choose strict only if the product truly demands it (rare). - Push vs poll. Real-time delivery must be push (WebSocket/SSE); polling for new comments at millions of viewers × several times/sec would melt the backend and still be laggy. The cost of push is a stateful connection fleet that must be balanced, drained, and failed over carefully.
- Ephemeral in-memory window vs durable store. Keeping only a rolling recent window in memory is cheap, fast, and enough for the live path. The trade is no deep scrollback — which the product doesn't need — plus an optional async sampled archive if VOD replay is wanted.
- Write-spike absorption. The per-video log/queue is the shock absorber that decouples spiky writers from controlled fan-out. Without it, a comment burst hits delivery directly; the cost is one more hop of latency on the post path (invisible, since posting is async anyway).
Extensions if asked
Add only the extension that changes the design discussion:
- Reactions / likes on the stream. Floating "🔥❤️😂" reactions are even higher-volume and lower-value than comments — perfect for aggregation: don't deliver each reaction; deliver a periodic count ("2,300 ❤️ in the last 5s"). Note the count must be aggregated centrally (or via a rollup tier) — a gateway only sees its own slice of posters, so an edge-only count would undercount the global total. Aggregate the global figure, then fan that out on the same tiered structure — aggregates, not individual events.
- Comment ranking / moderation at scale. Spam and abuse spike with the crowd. Run a fast moderation pass on the ingest path (profanity/spam filters, rate-limits per author, ML abuse scoring) before a comment enters the fan-out log, so garbage is dropped before it's amplified to millions. Ranking (surfacing high-quality comments) feeds directly into the sampler's prioritization.
- Replay for VOD. After the live event, viewers may re-watch the recording with comments. Asynchronously spool a sampled subset of comments (timestamped against the video position) to durable storage during the live event, then replay them positionally on VOD. This is a separate, non-hot-path write and explicitly not every comment.
- Pinned comments. The author or moderators pin a comment (a question, a link) that every viewer should see regardless of sampling. Pins bypass the sampler and are pushed to all gateways with a
pinnedflag, held at the top of the stream until unpinned — a tiny, always-delivered side-channel next to the sampled flow. - Per-region / per-language streams. For a global event, split the fan-out by region or language so viewers see comments they can read, and to keep any single topic's fan-out bounded — the tiered model already partitions naturally along these lines.
What interviewers look for & common mistakes
What interviewers usually reward:
- Recognizing the hot-channel shape — one video is one channel with millions of subscribers — and explicitly contrasting it with per-user chat fan-out, which does not transfer.
- Tiered / hierarchical fan-out — publish once to gateways via pub/sub, edge fan-out to viewers — so central work is O(gateways) and the O(viewers) amplification is distributed across the fleet, with no single owner.
- Naming the sampling insight — at extreme rates you cannot and should not deliver every comment to every viewer; rate-cap to a human-readable stream and prioritize author/friends/ranked comments, with the tradeoff stated.
- Absorbing the write spike with a per-video log/queue that decouples spiky ingest from controlled fan-out, plus idempotent posting.
- Approximate ordering via ingest time + per-video
seq, not a strict global total order (a needless bottleneck). - Connection management at scale — a geo-distributed gateway fleet, a
video → gatewaysregistry, mid-stream/reconnect backfill from a recent window, jittered reconnects, draining, and backpressure on slow consumers. - Leaning on ephemerality — a dropped comment is not a correctness bug, which licenses aggressive load-shedding and cheap backfill instead of durable per-viewer delivery guarantees.
Before you finish, do a quick mistake check:
- Did you treat one video as a single hot channel, not as per-user chat fan-out?
- Did you use tiered fan-out (pub/sub to gateways + edge fan-out) instead of one server owning a million-viewer channel?
- Did you recognize that at extreme scale you can't deliver every comment to every viewer, and design sampling/prioritization for it?
- Did you absorb the comment write spike with a log/queue rather than fanning out synchronously on the post path?
- Did you settle for approximate ordering instead of an unscalable strict global order?
- Did you address the connection-count problem — millions of WebSockets across a geo-distributed fleet, with reconnect/backfill, draining, and backpressure?
- Did you lean on ephemerality to justify load-shedding, rather than promising durable delivery of every comment?
Practice this live
Run this exact question with our AI voice interviewer and get feedback.
Start the interview →