Design Live Comments

Show comments on a live video to millions of viewers within seconds: SSE fan-out, pub/sub per video, sampling hot streams, history and ordering.

Case study30 minLesson 7 of 8
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

A cup final is streaming live and 5 million people are watching the same video. A goal goes in, and thousands of them type "GOAL" in the same second. Each viewer should see a lively stream of comments scroll past within a second or two, and someone who joins at half time should see what people were just saying.

The write side is small: a comment is a short line of text. The hard part is the read side: one write may have to reach millions of open connections, spread over a thousand servers, fast. A live video platform with a comment panel is the kind of product this question means. The steps follow A Framework for Any Design; try each "Your turn" box before you read on.

Clarify the problem

The first five minutes
Interviewer

Design live comments for a video streaming platform.

You

Only during live streams, or also when someone watches the recording later? And do viewers need every comment, or a readable stream of them?

Interviewer

Live only for now. A readable stream is fine; nobody can read thousands a second anyway.

You

What scale? Total viewers at peak, the biggest single stream, and how often people comment.

Interviewer

50 million concurrent viewers at peak across all streams, up to 5 million on one. A viewer comments about once every 10 minutes. About 2 billion comments a day.

You

Then fan-out is the design problem. Goals: a new comment reaches viewers within 2 seconds; a joining viewer sees recent comments at once; every comment is stored, but busy streams may show a sample. A viewer may miss a comment during a reconnect, but never see one twice. I'll leave out replies, likes and moderation tooling beyond a basic filter.

  • Functional: post a comment on a live video; see new comments as they arrive; see the most recent comments on joining; scroll back through older comments.
  • Non-functional: new comments shown within 2 seconds; every posted comment stored durably; up to 5 million viewers on one video; viewers never see duplicates; the comment panel failing must not affect the video.
  • Out of scope: replies, likes, recordings, the moderation console.

Estimate the scale

  • Comments at peak: 50 million viewers ÷ 600 seconds ≈ 83,000 comments a second.
  • On the biggest video: 5 million ÷ 600 ≈ 8,300 a second.
  • Daily average: 2 billion ÷ 86,400 ≈ 23,000 a second, so the peak is between three and four times the average.
  • Storage: 2 billion × 200 bytes = 400 GB a day, about 146 TB a year before replication.
  • Connections: 50 million open streams at an assumed 50,000 per server (load test it) is 1,000 realtime servers.
Estimate: Naive fan-out on the biggest video

If every comment on the biggest video went to every one of its viewers, how many deliveries per second would that be?

You know
  • 5 million viewers on the video
  • Each viewer comments once every 10 minutes

Show the working
  1. Comments: 5,000,000 ÷ 600 s ≈ 8,300 a second
  2. Deliveries: 8,300 × 5,000,000 ≈ 4 × 1010, about 40 billion a second

Estimate 40 billion/s (anything from 10 billion/s to 100 billion/s is a good answer)

Nobody can read 8,300 comments a second. Cap what is shown, and the problem shrinks by orders of magnitude.

Suppose you show at most 10 comments a second on any video and each server sends every viewer one batch every 500 ms. The biggest video then costs 5 million × 2 = 10 million small messages a second spread over 1,000 servers: 10,000 each. That is a lot, but it no longer depends on how fast people type.

Estimate: Realtime servers needed

How many realtime servers hold the open connections at peak?

You know
  • 50 million viewers at peak, one connection each
  • Assume 50,000 connections per server

Show the working
  1. 50,000,000 ÷ 50,000 = 1,000 servers
  2. Add spare capacity so a dying server's viewers have somewhere to go

Estimate 1000 (anything from 300 to 3000 is a good answer)

Your turn: Define the API

Write the calls for posting a comment, receiving new ones, and loading older ones. Which transport carries new comments to viewers?

Compare with a model answer

Posting is a plain HTTP request, since a viewer posts every few minutes. New comments flow down a server-sent events stream that resumes from the last event id. Older comments are a paged HTTP read with a cursor.

Define the API

HTTP and SSE
POST /videos/{id}/comments          body: { "text": "what a goal",
                                            "request_id": "r-51f" }
  201 { "comment_id": "7193...", "created_at": "..." }
  400 empty or too long   403 blocked or muted
  404 not live            429 posting too fast
 
GET /videos/{id}/live               Accept: text/event-stream
                                    Last-Event-ID: 4121 (on reconnect)
  id: 4122
  data: { "comments": [ { "comment_id": "7193...", "user": "ana",
                          "text": "what a goal" }, ... ] }
 
GET /videos/{id}/comments?before=7193...&limit=50
  200 { "comments": [...], "next_before": "7101..." }

The decision worth arguing is the transport. Viewers almost only listen, so use server-sent events rather than WebSockets: plain HTTP, friendly to proxies and load balancers, and browsers reconnect on their own, sending the last event id they saw. Posting is rare enough to be an ordinary POST, which gets authentication and rate limiting from the normal HTTP stack. A retry with the same request_id returns the first comment instead of posting it twice. The realtime communication lesson compares the options.

The history read pages with a cursor (before a comment id), not an offset. With 8,300 new comments a second, "page 2" would mean something different every time you asked.

Your turn: Model the data

What does a comment record hold, what is its key, and how do you read "the 50 before this one" on a video that gets thousands a second?

Compare with a model answer

Partition by video (plus a time bucket so no partition grows without limit) and sort by a time-ordered comment id inside it. Then the history read is one range scan, newest first.

Model the data

Records
comments                       -- wide-column store, about 200 B a row
  video_id     BIGINT
  bucket       INT             -- time bucket, sized per video
  comment_id   BIGINT          -- time-ordered 64-bit id
  user_id      BIGINT
  text         VARCHAR(200)
  status       TINYINT         -- visible | removed
  PRIMARY KEY ((video_id, bucket), comment_id DESC)
 
recent:{video_id}              -- in-memory list, last 200 shown
  [ { seq, comment_id, user, text }, ... ]

Writes are appends, reads are "newest N for a video, before an id", and nothing joins. A wide-column store such as Cassandra fits: partition by video, sorted by id, built for heavy writes. The time-ordered id gives newest-first order for free.

The bucket keeps partitions bounded. A quiet stream uses one-hour buckets. The biggest video at 8,300 a second would put 8,300 × 3,600 ≈ 30 million rows, 6 GB, in an hour's partition, so busy videos use one-minute buckets (500,000 rows, 100 MB). Store the bucket size on the video, so readers know which buckets to walk back through.

Your turn: High-level design

Sketch the write path from a POST to storage, and the path from there to 5 million open connections. How does a comment find the servers holding viewers of that video?

Compare with a model answer

The comment service stores the comment and puts it on a stream partitioned by video. A fan-out worker per partition samples busy videos and publishes to a pub/sub channel per video. Every realtime server with a viewer of that video is subscribed and pushes batches down its SSE connections.

High-level design

Writes and fan-out are split. The comment service makes a comment durable; the fan-out path decides what is shown and gets it to the right servers. The realtime servers only hold connections, so they can be restarted without touching storage.

Comments are stored, then flow through a stream partitioned by video to fan-out workers, which publish a sampled feed per video to every subscribed realtime server.Viewers to Realtime servers (SSE); Viewers to Comment service (POST, history); Comment service to Comment store (insert); Comment service to Comment stream (by video id), asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments (last 200); Fan-out workers to Pub/sub (video:77); Pub/sub to Realtime servers (subscribed); Realtime servers to Recent comments (on join).ViewersRealtime serversComment servicePub/subRecent commentsFan-out workersComment streamComment storeSSEPOST, historyinsertby video idlast 200video:77subscribedon join

request and replyasynchronous message

Comments are stored, then flow through a stream partitioned by video to fan-out workers, which publish a sampled feed per video to every subscribed realtime server.
Join, post, deliver, then a server diesUse the arrows or ← → keys
  1. Join, post, deliver, then a server dies, step 1Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serverssub video:77Comment servicePub/subRecent commentsFan-out workersComment streamComment store
    Ben's last id
    none

    1Ben opens video 77. The load balancer gives his SSE request to realtime server R1, which has no other viewer of 77, so it subscribes to video:77 first.

  2. Join, post, deliver, then a server dies, step 2Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsup to seq 4,121Fan-out workersComment streamComment store
    Ben's last id
    4121

    2Only then does R1 read the last 50 shown comments from the recent list and send them. Subscribing first means nothing can slip between the two; a comment in both is dropped by id.

  3. Join, post, deliver, then a server dies, step 3Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsFan-out workersComment streamComment store
    Ben's last id
    4121

    3Ana posts "what a goal". The comment service checks her rate limit and the word filter, and gives the comment a time-ordered id.

  4. Join, post, deliver, then a server dies, step 4Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsFan-out workersComment streamComment storestored
    Ben's last id
    4121

    4It inserts the comment into the store, then returns 201. Ana's own screen already shows it, so posting feels instant.

  5. Join, post, deliver, then a server dies, step 5Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsFan-out workersComment streamComment store
    Ben's last id
    4121

    5It appends the comment to the stream, keyed by video id, so every comment on video 77 lands in the same partition, in one order.

  6. Join, post, deliver, then a server dies, step 6Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsFan-out workersseq 4,122Comment streamComment store
    Ben's last id
    4121

    6The worker that owns that partition sees video 77 at 6 comments a second, under the cap of 10, so it keeps this one, numbers it 4,122 and adds it to the recent list.

  7. Join, post, deliver, then a server dies, step 7Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsFan-out workersComment streamComment store
    Ben's last id
    4122

    7It publishes to video:77. Pub/sub sends one copy to each subscribed server, which puts it in the next 500 ms batch to each local viewer. Ben sees it about a second after Ana posted.

  8. Join, post, deliver, then a server dies, step 8Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversdownComment servicePub/subRecent commentsup to 4,126Fan-out workersComment streamComment store
    Ben's last id
    4122

    8R1 crashes, dropping 50,000 connections. Comments 4,123 to 4,126 are published while Ben is away.

  9. Join, post, deliver, then a server dies, step 9Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversR2Comment servicePub/subRecent commentsFan-out workersComment streamComment store
    Ben's last id
    4126

    9Ben's browser waits a random few seconds and reconnects with Last-Event-ID: 4122. The new server, R2, already subscribed to video:77, reads 4,123 to 4,126 from the recent list and sends them. Ben missed nothing.

  10. Join, post, deliver, then a server dies, step 10Viewers to Realtime servers; Viewers to Comment service; Comment service to Comment store; Comment service to Comment stream, asynchronous; Comment stream to Fan-out workers, asynchronous; Fan-out workers to Recent comments; Fan-out workers to Pub/sub; Pub/sub to Realtime servers; Realtime servers to Recent comments.ViewersRealtime serversComment servicePub/subRecent commentsFan-out workerskeep 10 of 8,300Comment streamComment store
    posted per s
    8300
    shown per s
    10

    10The final whistle: video 77 jumps to 8,300 comments a second. All are stored, but the worker publishes only 10 a second, so pub/sub and the servers barely notice.

1 / 10

Deep dives

Interviewers push on getting one comment to millions of connections, very hot videos, and what "in order" and "no duplicates" really mean here.

Fan-out: which server holds which viewer

A viewer's SSE connection lands on whichever of the 1,000 realtime servers the load balancer picks. A new comment on video 77 must reach exactly the servers that hold viewers of 77. Something has to know that mapping.

Getting a video's comments to the servers holding its viewers
OptionGood atBad atPick it when
Firehose to every serverNo subscription state at all; each server filters for its own viewers.Every server receives every shown comment: at 83,000 a second unsampled and 1,000 servers, 83 million internal messages a second.Tens of servers and low comment rates.
Pub/sub channel per video Usual defaultA server gets only the videos its viewers watch. A server subscribes on its first viewer of a video and unsubscribes after its last.Subscriptions churn as viewers come and go, and a popular video is subscribed by nearly every server.Large scale with many videos. My choice.
Route viewers by video idConsistent hashing on video id puts a small video's viewers on one or two servers, so a comment goes to very few places.Load gets uneven, the load balancer must read the URL, and a video that goes viral must spill onto more servers.An optimisation for the long tail of small streams, on top of pub/sub.

With pub/sub, the pub/sub layer is the registry: each channel lives on one pub/sub node (chosen by hashing the video id), and that node knows which servers subscribed. A dead realtime server's subscriptions vanish with its connection, so stale entries clean themselves up. The biggest video ends up subscribed by all 1,000 servers, which is fine at 10 comments a second: 10 × 1,000 = 10,000 messages a second from one pub/sub node. Unsampled, the same video would be 8,300 × 1,000 ≈ 8.3 million a second from that one node.

Each realtime server then batches. It collects what arrived for each video over 500 ms and writes one SSE event per viewer. Batching turns "10 comments a second × 5,000 local viewers" into 2 writes a second per viewer, and the 500 ms wait fits easily inside the 2-second goal.

Very hot videos: sample, do not drop

When a video gets more comments than anyone can read, the panel needs a readable sample. The fan-out workers are the right place: the stream is partitioned by video id, so exactly one worker sees all of a video's comments and can count its rate.

Handling a video with 8,300 comments a second
OptionGood atBad atPick it when
Send everythingSimple; nothing is hidden.Unreadable for viewers and about 40 billion deliveries a second at the source.Never at this size.
Sample at the fan-out worker Usual defaultFan-out cost is capped at 10 a second per video whatever the posting rate. Every comment is still stored.Most comments on huge streams are never shown live, and posters may wonder why.The default for busy videos.
Slow mode for postersFewer comments to store and filter; less spam.Limits people, not the fan-out. A 30-second limit barely matters when people post every 10 minutes anyway.As a moderation tool alongside sampling.

How the worker samples: below the cap of 10 a second, everything passes. Above it, the worker always keeps comments from the creator and moderators, drops exact repeats ("GOAL" ×3,000 is noise), and fills the rest with a random pick from each 100 ms window. The poster always sees their own comment, because their client shows it as soon as the POST succeeds. The cost: on the biggest stream, almost everything people write is stored but never broadcast.

Joins spike too. If 1 million people join in the first minute, that is 1,000,000 ÷ 60 ≈ 17,000 recent-history reads a second, all for one key. Each realtime server reads the recent list for a video at most once a second and serves every joining viewer from that copy, so the cache sees at most 1,000 reads a second for the video.

Ordering, duplicates and history

Be precise about what you promise. Comment ids come from many comment servers whose clocks differ by a few milliseconds, so ids are only roughly in posting order. Live order is set elsewhere: all of video 77's comments pass through one stream partition and one worker, which numbers them 4,121, 4,122, and so on. Pick a pub/sub that delivers one publisher's messages on one channel in order (most do over a single connection), and every viewer sees the same sequence. History is sorted by comment id, so it can differ from the live order by a few milliseconds' worth of comments; nobody notices.

Pub/sub stores nothing, so a server that is restarting misses messages. Reconnects fill the gap from the recent list using Last-Event-ID; if the gap is longer than the 200 comments kept (20 seconds at 10 a second), the client skips ahead, which is right for a live panel. A comment service that dies between the insert and the stream append leaves a comment stored but never shown live, which "a sample is fine" allows. A restarted worker replays from its last committed position, so a comment can arrive twice; clients drop any comment id they have already shown. "Never twice on screen" comes from that check, not from the network.

Older history comes from the store: before an id, newest first, walking back through buckets. Paging is rare compared with live reading, so it can go to the database.

Bottlenecks and failures

At 10x (500 million viewers, 50 million on one stream), the realtime tier grows to 10,000 servers. Sampling keeps pub/sub load per video at 10 comments a second times the number of subscribed servers, so the hottest channel becomes 100,000 messages a second; add a relay layer so one pub/sub node feeds a few dozen relays that feed the servers. Storage grows to 4 TB a day, which a wide-column cluster handles by adding nodes, but the hottest video now sends 83,000 writes a second to one partition. Add a shard number from 0 to 15 to its partition key and have history reads merge the 16. The first thing to strain is the realtime servers' outbound bandwidth, because delivery cost grows with viewers, not with comments.

When this diesWhat happensWhat you do
A realtime server50,000 viewers lose their stream; the video keeps playing.Browsers reconnect with jitter (over 30 seconds, about 1,700 a second) and resume from Last-Event-ID.
A pub/sub nodeChannels on it stop delivering.Realtime servers resubscribe on the node that takes over and fill gaps from the recent list.
A fan-out workerIts videos stop updating live; comments are still stored.The stream hands its partitions to another worker. It continues numbering from the last seq in the recent list, skips comments already there, and resumes from the last committed position.
The recent-comments cacheJoiners start with an empty panel; reconnects cannot fill gaps.Run replicas; rebuild a video's list from the newest bucket in the store.
A comment store nodeIts replicas elsewhere keep serving; with 3 replicas and quorum writes, posting works while 2 are up.Replace the node. If a partition loses 2 replicas, posting fails with 503 while viewers keep reading.

What a strong answer sounds like

LevelWhat the answer does
JuniorStores comments and has clients poll or hold a WebSocket; with help, realises the sender and viewers sit on different servers.
MidChooses SSE or WebSockets with a reason, routes through pub/sub channels per video, and loads history with a cursor.
SeniorComputes the naive fan-out, agrees a display cap, samples at a per-video worker, explains what ordering and duplicate guarantees hold and where they come from, and keeps the comment panel from affecting the video.
The hottest videoStrong answer
Interviewer

One stream has 5 million viewers and 8,300 comments a second. What breaks?

You

Sending everything would be about 40 billion deliveries a second, and nobody can read 8,300 a second anyway. So the worker that owns that video's partition caps the live feed at 10 a second: creator and moderators first, repeats removed, a random pick of the rest. Every comment is still stored. That makes the pub/sub load 10 messages times 1,000 servers, and each server sends each viewer two batches a second.

Interviewer

Won't people complain their comment never appeared?

You

Their own client always shows it, so they see it posted. On a stream that size, being seen by everyone was never possible; the product question is who gets priority.

Follow-up questions

How would comments replay with the recording?

Store each comment's offset into the video when it was posted. During playback the client fetches comments for the next minute of video by offset and shows each at its moment. That is a cacheable read, not a realtime problem.

A moderator removes a comment that was already shown. What happens?

Mark it removed in the store and publish a small "remove 7193..." event on the video's channel. Clients delete it from the panel, and the recent list drops it.

How would you add live heart reactions?

Reactions arrive far faster than comments and nobody needs each one. Count them per video per second at the fan-out worker and broadcast only the counts.

How would you run this across regions?

Give each region its own realtime servers and pub/sub. The worker owning a video publishes once to each region, so a comment crosses an ocean once, not once per viewer.

How do you stop spam and bots?

Rate-limit posting per account and per IP, require a verified account to comment on big streams, and filter text before publishing. Sampling also mutes floods: 1,000 copies of one link are one repeat.

Self-review: live comments0 of 12

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure