Design live comments
A cricket final is streaming live to five million people, and thousands of them are typing at once. Every viewer should see new comments within a couple of seconds, scroll back through what they missed, and never see abuse that moderators removed. The difficulty isn't storing comments. It's delivering one stream of them to millions of open connections, and deciding what a human can actually read.
1. The prompt
"Viewers of a live video can post comments. Everyone watching sees new comments appear in near real time. Design it."
The write side is modest; the read side is a fan-out problem. A comment written once may need to reach millions of screens within two seconds, over connections that drop whenever a phone goes into a tunnel. A good answer separates storing comments, routing them to the right servers, and delivering what a person can read.
2. Frame the problem
Ask these in the first five minutes. Each answer changes the design; guess before you open it.
How big is the biggest stream?
Most streams have a few dozen viewers. The largest events reach about 5M concurrent viewers.
The design has to cover both a long tail of tiny streams and a few huge ones. The huge ones decide the fan-out design.
How fast must a comment appear for others?
Within about 2 seconds for most viewers.
Two seconds rules out per-viewer database polling and gives a budget to split between write, routing and delivery.
Must every viewer see every comment?
On small streams, yes. On a stream with thousands of comments a second, nobody can read them all, so showing a representative, relevant subset is fine.
This is the most important question. It turns an impossible fan-out into a manageable one.
Do comments need a strict order?
Roughly in time order for everyone. Two comments a second apart may appear in either order.
We can avoid a global sequencer and use per-stream, time-ordered ids.
Can viewers scroll back, and do comments outlive the stream?
Yes. Viewers can load earlier comments, and comments replay alongside the recording afterwards.
Comments must be stored durably, keyed by stream and time, with the video timestamp kept for replay.
Are there replies, reactions, or only flat comments?
Flat comments now. Reactions (hearts, emoji) exist but are counted, not shown one by one.
Reactions are aggregated counters (Design D17 style), not messages.
What moderation is needed?
Banned users and blocked words must never appear. A moderator or classifier can remove a comment, and it must disappear from screens within seconds.
We need a fast pre-publish check, and "delete" events that travel the same path as comments.
Which clients?
Phones (often on mobile data), browsers and TVs.
Connections drop constantly, so reconnect-and-resume is a core path. Bandwidth per viewer matters.
3. What it must do
- A viewer can post a comment on a live stream.
- Viewers see new comments as they arrive, without refreshing.
- A viewer can load earlier comments by scrolling back, during and after the stream.
Not now (with a reason):
- Replies and threads: they change the data model and the UI, and aren't needed for a live chat.
- Individual reactions: counted and shown as totals instead.
- Search over comments: a separate indexing job.
- Translation: an optional client feature on top.
4. How well it must do it
| Target | Why this number |
|---|---|
| Fresh: 99% of viewers see a new comment within 2 s of posting speed | Live chat feels broken when replies arrive after the moment has passed. |
| Scale: 50M concurrent viewers in total, 5M on one stream, 200,000 comments a second at peak across all streams load shape | Big events are bursty: a goal or a wicket makes comments jump tenfold in seconds. |
| Available over consistent: during failures, viewers keep seeing comments, even if a few arrive late or out of order consistency or availability | A frozen feed is worse than a slightly imperfect one. No money or safety depends on exact order. |
| Kept: an accepted comment is stored and visible in scrollback and replay data safety | The creator and moderators rely on the record. A few seconds of delivery loss is tolerable; storage loss isn't. |
| Safe: banned users and blocked content never appear; removed comments vanish from screens within 5 s security · rules | Large live events attract abuse, and platforms have legal duties to act on reported content. |
| Light on phones: under ~5 KB/s of comment data per viewer where it runs | Viewers are already streaming video on mobile data. Comments must not compete with it. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Open connections | 50M viewers; a tuned server holds 50–100K long-lived connections | ~500–1,000 connection servers → so delivery is a fleet of stateful servers, separate from the comment API |
| Naive fan-out on the biggest stream | 5M viewers × 500 comments/s | 2.5 billion deliveries a second for one stream → so we must not send every comment to every viewer |
| What a human can read | Comfortably 5–10 short comments a second | → so cap each viewer at ~10 comments/s, batched once a second: 5M messages/s for the stream, spread over ~100 servers |
| Bandwidth per viewer | 10 comments × ~250 bytes once a second | ~2.5 KB/s: within the phone target |
| Writes | 200,000 comments/s × ~300 bytes | ~60 MB/s, ~5 TB a day → so a partitioned log in front of a wide-column store, partitioned by stream and time bucket |
Skip estimating total storage for years or API calls for history. They're ordinary, and nothing in the design depends on them.
6. The nouns
- Stream: a live video, with its status (live or ended) and owner.
- Comment: text by a user on a stream at a moment.
- User: with ban status.
- Connection: one open viewer session on a connection server.
- Moderation action: a removal or a ban.
comments partition (stream_id, minute_bucket) · sort comment_id (time-ordered, e.g. Snowflake)
user_id, text, video_offset_ms, status (visible | removed), score
streams stream_id, owner_id, status, started_at, viewer_count (approximate)
bans (stream_id or global, user_id) → until
7. The contract
Posting is an ordinary REST call. Live delivery is one-way from server to viewer, so Server-Sent Events fit: plain HTTP, built-in reconnection, and the browser resends the last event id it saw, so the server can resume. WebSockets work too, and are the choice if the same connection must also carry chat typing indicators or other client-to-server traffic. The user always comes from the session token.
POST /v1/streams/{stream_id}/comments {text, client_id: "c-4f1…"}
→ 201 {comment_id, created_at} (client_id dedupes a retried post)
GET /v1/streams/{stream_id}/comments?before={comment_id}&limit=50 → older comments (scrollback)
GET /v1/streams/{stream_id}/live Accept: text/event-stream
← id: 7244031 event: comments data: [{id, user, text}, …] (a batch about once a second)
← id: 7244032 event: removed data: ["7243990"]
← event: stats data: {viewers: 4812330, hearts_per_s: 9100}
DELETE /v1/comments/{comment_id} (author or moderator)
Data flow: (1) a comment is checked and accepted → (2) stored, and appended to the stream's log partition → (3) routed to the connection servers that hold viewers of that stream → (4) each server batches, samples and sends once a second → (5) viewers that reconnect resume from their last event id.
8. First working version
Start with an API service, one database and a few connection servers. Get all three requests working, and note what won't scale.
8.1 Posting a comment
The API checks the user isn't banned, rate-limits them (say 1 comment every 2 s per user per stream), assigns a time-ordered id and writes the comment. The client_id makes a retried post (the phone lost the response) return the same comment instead of a duplicate.
8.2 Watching live (version one)
The viewer opens an SSE connection to a connection server. In this first version, the server asks the database once a second for comments newer than the last one it sent to each viewer, and pushes them.
8.3 Scrolling back
A history request reads the newest partition (stream_id, current minute) backwards from the given id, then moves to earlier minute buckets until it has 50 comments. A CDN can cache history pages of ended streams, because they never change.
Parked for later (section 9 fixes each)
- Each server polling the database for each of its viewers: 50M queries a second.
- A 5M-viewer stream receiving 500 comments a second would mean billions of deliveries.
- All writes for the biggest stream land on one database partition.
- Phones reconnecting lose the comments sent while they were away, or get them twice.
- Abusive comments go out before anyone can stop them; removed ones stay on screens.
- Deploying or losing a connection server drops 50,000 viewers at once.
9. Make it hold up
9.1 Getting each comment to the right servers fresh · scale
| Approach | Why | |
|---|---|---|
| ✗ Bad | Every connection server polls the database for every viewer | Load grows with viewers × polls a second. The database falls over long before 50M viewers. |
| ~ Good | Every connection server subscribes to the whole comment stream and keeps only what its viewers need | No polling, but 1,000 servers each receiving 200,000 comments a second is 200M messages a second of mostly wasted work. |
| ✓ Great | Two-level fan-out by stream: comments are published to a per-stream channel; a connection server subscribes to a channel only while it has viewers of that stream, then fans out locally to its own connections. Route viewers of the same stream to the same group of servers (consistent hashing on stream_id) so each channel has few subscribers. | Each comment crosses the network once per server that needs it, not once per viewer. Small streams live on one server; a huge stream spreads over a group sized to its audience. |
The channel layer can be Redis pub/sub (fast, fire-and-forget) or a log such as Kafka (replayable, with retention). A common split: Kafka partitions hold the durable comment log; a lightweight pub/sub tier does the live fan-out to connection servers; the log is the source for recovery.
9.2 The five-million-viewer stream fresh · light on phones
During a big moment, a top stream can receive 500 or more comments a second. Delivering them all would cost billions of messages a second, and no human could read them anyway. The answer is product and engineering together:
- Batch: each connection server sends one message per viewer per second containing that second's comments. Messages per second become viewers × 1 instead of viewers × comments.
- Sample by relevance: when a second has more comments than a viewer can read (say more than 10), choose which to show. Prioritise the creator and moderators, the viewer's friends, verified accounts and high-quality comments (a score from the classifier), then fill with a random sample. Every server can apply the same rules.
- Personal first: a viewer always sees their own comment immediately, inserted by their client, even if the sample left it out for others.
- Reactions as counts: hearts are counted per second per server and summed; viewers receive "9,100 hearts/s" and the client animates floating hearts locally.
The lab in section 13 shows the difference on a phone screen.
9.3 Storing comments without a hot partition kept · scale
| Approach | Why | |
|---|---|---|
| ✗ Bad | A relational table with an index on (stream_id, created_at), one primary | 200,000 writes a second on one primary is too many, and the biggest stream's index pages become a hotspot. |
| ~ Good | A wide-column store (Cassandra, DynamoDB) partitioned by stream_id, sorted by comment id | Scales across streams, but one huge stream is one partition. A DynamoDB partition, for example, accepts up to 1,000 write units a second. |
| ✓ Great | Partition by (stream_id, minute_bucket), and write through a log: the API appends to a Kafka partition (by stream), and a consumer writes to the store in batches | Each minute of a busy stream is a new partition, so writes spread over time. The log absorbs the spikes after a goal, and delivery can read from the log immediately while storage catches up. |
9.4 Ordering, reconnects and duplicates fresh · kept
- Time-ordered ids: comment ids embed a timestamp (a Snowflake-style id, Module 20), so sorting by id is sorting by time. No global sequencer is needed, and two comments a second apart may swap, which the requirements allow.
- Batch ids for resume: each live message carries an SSE
id. When a phone reconnects, the browser sendsLast-Event-IDautomatically, and the server replays what was missed from a short in-memory buffer of recent batches per stream (the last two minutes). Longer gaps fall back to a history read. - Dedupe on the client: the client keeps the ids it has shown, so a batch replayed after a reconnect doesn't show comments twice.
- Reconnect politely: clients retry with exponential backoff and jitter. The server can send an SSE
retry:value to spread reconnections after an incident.
9.5 Moderation that keeps up safe
| Approach | Why | |
|---|---|---|
| ✗ Bad | Publish everything and rely on viewer reports | On a big stream, abuse reaches millions of screens before anyone reports it. |
| ~ Good | Synchronous checks before accepting a comment: ban list, rate limit, blocked words | Fast (a few milliseconds) and stops the obvious cases, but misses anything subtle. |
| ✓ Great | Synchronous checks, plus an asynchronous classifier that scores every comment within about a second. On big streams, hold comments until the score is in (the sampler prefers high scores anyway); on small streams, publish immediately and remove if flagged. Removals travel as removed events on the same channels. | Safety where the audience is largest, speed where it's smallest. Removal reaches every screen in the same ~2 s as comments. |
Also give creators their own moderators, slow mode (a longer per-user rate limit), "followers only" mode, and shadow bans (the banned user still sees their own comments, so they don't simply create a new account).
9.6 Running the connection servers scale · surviving failures
- Stateful but disposable: a connection server holds open connections and its channel subscriptions, nothing else. If it dies, its viewers reconnect elsewhere and resume from their last event id.
- Tuning for many connections: event-driven servers, raised file-descriptor limits, small per-connection buffers, idle heartbeats every 15–30 s so proxies don't drop quiet connections. LinkedIn has written about serving hundreds of thousands of SSE connections per machine this way.
- Draining on deploy: never restart 50,000 connections at once. Stop accepting new connections, then close existing ones gradually over a few minutes with a
retryhint, so the reconnections spread out. - Load balancing: balance on connection count, not request rate. Route by
stream_idso viewers of the same stream share servers, with a group of servers per huge stream.
9.7 The whole design after the deep dives
Follow one comment through every part: checks, the log, storage, per-stream channels, connection servers that batch and sample, and the viewers' phones.
10. Interviewer follow-ups
Expect several of these after the main design. Give the mechanism and its cost in under a minute.
A World Cup final: 50M viewers on one stream.
Sampling already caps each viewer at ~10 comments a second, so delivery scales with viewers: 50M messages a second over ~1,000 servers. Add a third fan-out level: regional relay servers subscribe once to the stream channel and feed the connection servers in their region, so the channel has dozens of subscribers instead of a thousand. Pre-scale before kick-off; the schedule is known.
A region goes down.
Its viewers reconnect to the next region through DNS or anycast failover, with jittered retries. Comments written there and already in the replicated log still appear; the last second or two may be lost from live delivery but are in storage if the write was acknowledged. Availability wins: viewers see the feed again within seconds.
The streamer posts a comment. Does everyone see it?
Yes. The sampler always includes the creator's and moderators' comments, and the client can pin them. Priority rules beat random sampling.
Comments should replay alongside the recording.
Store video_offset_ms with each comment (the position in the stream when it was posted). The replay player requests comments by offset range as the video plays: "offsets 120,000–150,000 ms". Precompute sampled replay pages for huge streams and serve them from a CDN.
Why not send comments as push notifications?
Push notifications are for waking a device that isn't connected, with delivery in seconds to minutes and strict rate limits. Live comments need sub-second, high-rate delivery to an app that's open, which is exactly what an open SSE or WebSocket connection gives.
Why SSE and not WebSockets?
Traffic is almost all server-to-client; posting is a normal POST. SSE is plain HTTP (friendly to proxies and HTTP/2), reconnects by itself and resends the last event id, so resume is nearly free. Choose WebSockets if the connection must carry two-way traffic, such as typing indicators or interactive polls.
How do you show the live viewer count?
Each connection server knows its connection count per stream. Every few seconds it reports to an aggregator, which sums them and publishes stats events. It's approximate by design; nobody needs the exact count.
A legal request: remove every comment by a user, everywhere.
Mark the user banned, emit removed events for their comments on live streams, and run a background job that sets status = removed in storage and purges cached history pages. Log the action for audit.
Costs must drop by half.
Lower the per-viewer cap during calm periods (5 a second is still plenty), batch every 2 s on TV clients, compress batches, move ended streams' comments to cheaper storage, and keep small streams on shared servers. Bandwidth to clients is usually the biggest bill, so the cap is the biggest lever.
Bots flood a stream with near-identical spam.
Per-user and per-IP rate limits (Design D02), near-duplicate detection on text (hash after normalising), account-age and reputation checks, and automatic slow mode when the spam rate spikes. The classifier's score also pushes spam out of the sample.
Make it persistent community chat instead.
Then history matters as much as live delivery: messages are stored by channel and time window, members have read markers, and big servers push full messages only to members viewing the channel. Design D07's community servers variation walks through it.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| A connection server dies | Its viewers' feeds pause for a few seconds | Connection count drops; health checks | Clients reconnect with jitter and resume from Last-Event-ID |
| Pub/sub node fails | Some streams pause briefly | Publish errors; subscription loss | Servers resubscribe on a replica; gaps refilled from the log |
| Storage slows down | Live feed fine; scrollback slow | Write lag from the log consumer | Delivery reads from the log, not storage; consumer catches up |
| Classifier is down | Big streams' comments delayed or reduced | Scoring latency alert | Fall back to synchronous checks plus stricter sampling and slow mode |
| A goal triggers 10× comments | Nothing, ideally | Comment rate per stream | Log absorbs the burst; sampling caps delivery; pre-scaled servers |
| Reconnect storm after an outage | Slow recovery | Connection attempts per second | Jittered client backoff, server retry: hints, admission control |
12. Wrap-up and the clock
Trade-offs we made:
- Sampling instead of "everyone sees everything" on huge streams.
- Rough time order instead of a global sequence.
- SSE for one-way delivery.
- Availability over exactness during failures.
- Holding comments for scoring only where the audience is large.
What we'd watch:
- Post-to-screen latency p99, measured by synthetic viewers.
- Connections per server.
- Comment rate per stream.
- Removal-to-screen time.
- Reconnects per minute.
- Bytes per viewer.
At 10×:
- Regional relay tiers.
- Larger server groups per stream.
- Stricter caps for the largest audiences.
A 60-minute interview
| 0–5 | Frame the problem: biggest stream, "must everyone see everything?" |
| 5–10 | Must do, how well, the fan-out numbers |
| 10–15 | Nouns and contract (SSE vs WebSockets) |
| 15–28 | First working version 8.1–8.3, parked list |
| 28–50 | Deep dives: two-level fan-out (9.1), the huge stream (9.2), resume and order (9.4) |
| 50–60 | Moderation or storage, then follow-ups and wrap-up |
A 90-minute interview
| 0–8 | Frame the problem, including moderation and replay |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, contract, data flow |
| 22–38 | First working version |
| 38–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: 50M viewers, region loss, cost |
| 85–90 | Trade-offs, metrics, 10× plan |
| Level | What a strong answer shows |
|---|---|
| Mid-level | A comment API, a store keyed by stream and time, persistent connections (SSE or WebSockets) and pub/sub instead of polling; history with cursors. |
| Senior | Two-level fan-out by stream with routing by stream_id; the huge-stream insight (batch and sample); hot partitions fixed with time buckets and a log; resume with event ids; pre-publish checks and removal events. |
| Staff+ | Frames the product trade-off (what people can read) with numbers, relay tiers for mega-events, moderation latency vs audience size, connection-fleet operations (draining, reconnect storms), cost per viewer and regional failover. |
Common mistakes:
- Polling the database per viewer.
- Promising every comment to every viewer of a 5M stream.
- One partition per stream with no time bucket.
- Forgetting reconnects on mobile.
- Treating moderation as an afterthought.
- Using push notifications for live delivery.
13. Try it, check yourself, read more
The comment firehose. The phone shows comments arriving at the rate this viewer would actually receive them. The numbers show what the servers must send.
Illustrative capacity: one connection server holds 50,000 viewers and sends up to 500,000 messages a second; a comment is ~250 bytes.
How does a comment reach millions of viewers without polling?
A stream has 5M viewers and 500 comments a second. What do you send each viewer?
A phone's SSE connection drops for 20 seconds. How does it catch up?
How do you stop the biggest stream from overloading one storage partition?
| Resource | Why |
|---|---|
| Instant messaging at LinkedIn: scaling to hundreds of thousands of persistent connections (LinkedIn) | SSE connection servers in production, and the limits they hit. |
| Server-sent events (HTML standard) | Event ids, Last-Event-ID and retry. |
| Design Facebook live comments (Hello Interview) | Another walkthrough of the same problem. |