Design live comments and reactions for live streams
The interviewer asks
“Design live comments and emoji reactions for a live stream, like chat during an India vs Pakistan match on JioHotstar or a YouTube Live. Tens of millions of people watch the same stream; comments and reaction bursts should appear for viewers within a couple of seconds.”
What a strong answer looks like
The core insight is that this is a fan-out problem with a lossy, human-sized output. No viewer can read 50,000 comments a second, so a strong design sends every viewer a sampled, moderated stream of a few comments per second, and turns millions of reactions into aggregated counts per second rather than individual events. Viewers hold a persistent connection (WebSocket or SSE) to a fleet of edge gateways; each gateway subscribes once per stream to a pub/sub channel, so fan-out is two-level: one message to hundreds of gateways, then each gateway pushes to its tens of thousands of local connections. Writes go through rate limiting, moderation and an aggregator. Durability is weak by design for reactions, stronger for comments that are stored for replay.
Concepts to mention
1 Requirements
Functional
- Viewers post text comments on a live stream and see others' comments in near real time
- Viewers send emoji reactions; everyone sees bursts (for example floating hearts) and totals
- Moderation: block abusive comments, slur filters in Indian languages, user mute and ban
- Pinned comments and highlighted comments from hosts or verified users
- Comments stored so that a replay (VOD) shows them at the right time offset
Non-functional
- Up to 50M concurrent viewers on one stream (a big cricket match), 10k+ streams in parallel on a normal day
- End-to-end delivery under 2 s p95 from post to other viewers' screens
- Write rate: up to 100k comments/s and 2M reactions/s on one hot stream
- Losing a few reactions is acceptable; accepted comments should not be lost for replay
- Chat failure must never affect video playback
Out of scope (say it out loud)
- Video streaming pipeline itself
- Private one-to-one messaging
- Paid super-chat payments (mention as extension only)
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
WS/v1/live/{stream_id}/connect | Subscribe to comment and reaction frames for a stream; server pushes batched frames |
POST/v1/live/{stream_id}/comments | Post a comment (text, client_msg_id for idempotency); returns accepted or rejected |
POST/v1/live/{stream_id}/reactions | Send reactions; client batches taps, e.g. {heart: 7} every 2 s |
GET/v1/live/{stream_id}/comments?from_offset=&to_offset= | Fetch stored comments for replay or for catch-up after reconnect |
POST/v1/live/{stream_id}/moderation | Host or moderator actions: pin, delete, mute user, slow mode |
4 Data model
commentsCassandra / ScyllaDBstream_idbucket (minute)comment_id (time-ordered)user_idtextstream_offset_msstatusPartition by (stream_id, minute bucket) to avoid one giant partition per match; clustering by comment_id for time order. TTL for non-replay streams.
stream channel stateRedisstream_id → pinned commentslow-mode secondsviewer countbanned users setSmall, hot, read by every write path request.
user moderationPostgres + Redis cacheuser_idglobal banmute_untiltrust_scoreTrust score used to sample and prioritise comments.
reaction aggregatesIn-memory in aggregators, snapshots to Redisstream_idsecondemoji → countTotals persisted every few seconds; individual taps are never stored.
pub/sub channelsKafka (ingest) + Redis pub/sub or NATS (fan-out)topic per stream shard / channel per stream_idKafka for durable ingest; light pub/sub for fan-out to gateways.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Post a comment
- 1Viewer sends POST /comments with text and client_msg_id over HTTP (or over the same WebSocket)
- 2Write API checks auth, ban and mute lists and slow-mode in Redis, and applies a per-user rate limit (e.g. 1 comment per 3 s)
- 3Fast moderation: blocklist and regex for slurs in English, Hindi and Hinglish, link spam, repeated text; an ML toxicity model with a ~20 ms budget
- 4Accepted comment is written to Kafka, partitioned by stream_id (hot streams are split across several partitions by hashing user_id)
- 5The sender sees their own comment immediately (optimistic local echo), whether or not it is sampled for others
- 6A consumer writes the comment to Cassandra with its stream offset for replay
Deliver comments and reactions to 50M viewers
- 1Sampler/aggregator for the stream consumes the Kafka partitions and every ~500 ms builds a frame: up to ~5 comments chosen by priority (host, verified, high trust, diversity) plus reaction counts
- 2It publishes the frame once to the stream's pub/sub channel
- 3Each gateway with at least one viewer of that stream is subscribed to that channel, so ~1,000 gateways receive the frame, not 50M connections
- 4Each gateway serialises the frame once and writes it to all its local connections for that stream
- 5Client animates reactions based on counts (e.g. scale the number of floating hearts) and appends comments
Reconnect after network drop
- 1Client reconnects with exponential backoff and jitter to avoid a thundering herd
- 2It sends the last frame sequence number it saw
- 3Gateway resumes live frames; optionally the client fetches the last 30 s of stored comments from the read API; missed reactions are simply skipped
7 Deep dives
Two-level fan-out and the gateway fleet
Pushing directly from one service to 50M sockets is impossible, so the fan-out is hierarchical. Viewers connect through an L4 load balancer to stateless WebSocket gateways (each holding ~50k-100k connections using epoll-based servers in Go, Rust, Java Netty or Erlang). Each gateway keeps a local map stream_id → set of connections. When a gateway gets its first viewer for a stream, it subscribes to that stream's channel on the pub/sub layer; when the last one leaves, it unsubscribes. So the publish fan-out is 1 message → number of gateways (hundreds), and each gateway does the local fan-out with a single serialisation of the frame. For a stream with 50M viewers the pub/sub cost is trivial; the gateways' network output is the real load: at 2 KB per frame and 2 frames/s, 50M viewers need ~200 GB/s of egress across the fleet, which is why frames are batched and compressed (permessage-deflate) and why the frame rate is capped. For very large events add one more level: regional relay nodes subscribe to the core channel and re-publish to gateways in their region, which also keeps cross-region traffic low.
Sampling comments: what to show when 100k arrive per second
Every viewer receiving every comment is both impossible and useless. The sampler per stream receives all accepted comments and picks a small set per tick (for example 5 per 500 ms). Selection uses priority tiers: pinned and host messages always; verified users and high-trust accounts next; then a random sample from the rest, weighted to avoid duplicates (collapse 10,000 'what a shot!!' into one) and to mix languages. Different viewers can get different samples: shard viewers into ~16 cohorts and generate a slightly different frame per cohort, so more distinct comments get seen overall, while each frame is still shared by millions. The sender always sees their own comment locally, which keeps the experience feeling responsive. For small streams (under ~1,000 comments per minute) the sampler just passes everything through. This is the same idea as load shedding: you degrade gracefully by design rather than falling over. Mention it explicitly; interviewers look for the candidate who notices the human reading limit.
Reactions as aggregated counters
Two million taps per second cannot be individual messages. Clients batch locally: tap 15 hearts in 2 seconds, send {heart: 15} once. The write path does not even need Kafka durability for reactions: send them to aggregator nodes sharded by stream_id (and sub-sharded for hot streams), which keep in-memory counters per second. Every 500 ms to 1 s each shard emits partial counts; a merger combines them and puts {heart: 120k, six: 40k} into the next frame. Clients turn counts into animation density, capped so the UI stays smooth. Totals are snapshotted to Redis every few seconds for display and analytics. If an aggregator dies, you lose at most a second of reactions, which nobody notices. This is a deliberate choice: weak durability for high-volume, low-value events, strong durability for comments that appear in replays and can be reported. Showing you understand that different data in the same feature deserves different guarantees is a strong signal.
Moderation at live speed
Moderation must fit in the write path budget (tens of milliseconds) because sampled comments go out within a second. Layer it. First, cheap rules: blocklists for abusive words in English, Hindi, Tamil and others including romanised spellings and leetspeak, URL and phone number blocking, repetition and flood checks. Second, a small toxicity classifier served on CPU with a strict timeout; if it times out, the comment is still stored but marked not eligible for sampling. Third, user-level controls: per-user rate limits, slow mode for the whole stream during spikes, new-account restrictions, and a trust score. Fourth, asynchronous heavier models and user reports that can retroactively delete comments; deletions are broadcast as a small 'remove comment_id' event. Because only a sampled fraction is shown, the sampler can prefer high-confidence clean comments, which further reduces risk. Legal requirements in India (IT Rules) mean you need takedown audit logs too.
8 Trade-offs
WebSocket vs SSE vs long polling
Viewers mostly receive, so SSE over HTTP/2 is enough and simpler through proxies; posts go over normal HTTP. WebSocket gives bidirectional traffic on one connection and is the common choice in apps. Long polling multiplies request load and is a fallback only. Either way the gateway architecture is the same.
Kafka vs Redis pub/sub for fan-out
Kafka is durable and great for ingest and persistence, but having 1,000 gateways each consume per-stream topics is awkward. Redis pub/sub or NATS is fire-and-forget and very fast for fan-out. Use Kafka for ingest and storage, light pub/sub for the broadcast hop; frames lost there are acceptable.
Everyone sees the same comments vs per-cohort samples
One global sample is simple and consistent; per-cohort sampling shows more distinct comments overall and spreads attention. Since there is no 'correct' set of comments to show, cohorts are a cheap win.
Sync moderation vs async
Full ML moderation synchronously adds latency and a dependency; fully async lets abuse show for seconds to millions. A fast sync layer plus async heavy checks with retroactive deletion balances both.
9 Bottlenecks
Connection storm at the start of a match or after an outage
Millions reconnect within seconds. Use jittered exponential backoff on clients, connection rate limits per gateway, pre-scaled gateway fleets before scheduled events, and let gateways accept connections before subscribing to streams lazily.
One hot Kafka partition per stream
A single partition caps throughput. Split the hot stream's topic into many partitions keyed by user_id and have the sampler merge them; ordering across comments does not matter at the second level.
Gateway egress bandwidth
Output dominates. Batch frames, compress, cap frame size and frequency, and drop to fewer comments per frame under load.
10 Failure handling
Gateway crash
Its 100k clients reconnect via the load balancer to other gateways with jittered backoff and resume from the latest frame. No state is lost because gateways are stateless.
Pub/sub node failure
Run the fan-out layer clustered with channels sharded across nodes; gateways resubscribe on failover. A few seconds of frames may be lost, which is acceptable for live chat.
Comment pipeline overloaded
Turn on slow mode automatically, tighten rate limits, raise the sampling threshold, and keep video playback completely independent so chat issues never affect the stream.
11 What impresses
- Noticing that humans can read only a few comments per second and designing sampling around it
- Computing gateway count and egress bandwidth, not just QPS
- Treating reactions as counters, not messages
- Explaining why chat failure must be isolated from video playback
- Per-cohort sampling to show more distinct comments
- Pre-scaling for scheduled events like IPL finals
12 Mistakes
- Broadcasting every comment to every viewer
- Storing every reaction tap in a database
- One service pushing directly to millions of sockets with no gateway tier
- Ignoring reconnect storms
- No moderation in the write path
- Using a single Kafka partition per stream and not noticing the throughput cap
13 Follow-ups
Lessons this design uses
Practice questions
- During an IPL final, your Cassandra table of 'reactions per match' (partition key match_id) is timing out. Why, and how do you fix the model?
- To fan out events to your WebSocket gateways, a teammate suggests Redis pub/sub and another suggests Kafka. Compare them for this use case.
- During a cricket final with 25 million live viewers, you want to show live comments and emoji reactions on the stream. Design how reactions and comments reach everyone.
- During a cricket final, 20 million viewers react to one wicket within seconds. Your reaction counter for that match is a single Redis key, and that Redis shard is at 100% CPU. How do you redesign it?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.