Design Live Comments

Design live comments and reactions for a video platform's live streams. While watching a stream, viewers can post short comments and tap reactions (hearts, laughs and so on). Every viewer sees a scrolling list of recent comments and floating reactions alongside the video, close to real time.

Most streams have a few dozen viewers, but a major sports final or product launch can have 10 million people watching one stream at once, with tens of thousands of comments and hundreds of thousands of reactions arriving every second. No human can read that many comments, and no network should try to send them all.

In 45 minutes, design the write path for comments and reactions, how updates reach millions of open connections, and what each viewer actually receives. Cover moderation and what happens when the stream is replayed later.

Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.

Design Path · Social & Messaging · problem 12 of 70

Ask before you design. Try answering each yourself, then compare.

  1. Must every viewer see every comment?
    Typical answer

    No. Small streams show everything; very busy streams show a representative, filtered sample. The comment author always sees their own comment.

  2. What end-to-end latency is acceptable?
    Typical answer

    Comments should appear for other viewers within about 2 seconds. Ordering across viewers only needs to be roughly consistent.

  3. What is the peak scale?
    Typical answer

    Across the platform, around 50 million concurrent viewers. A single stream can reach 10 million viewers with 50,000 comments/s and 500,000 reactions/s.

  4. Do comments need to be stored?
    Typical answer

    Yes, for moderation, for the creator's dashboard, and for replay: when the recorded stream is watched later, comments appear at the video time they were posted.

  5. What moderation is required?
    Typical answer

    Block banned words and links instantly, rate limit each user, let the creator's moderators delete comments and ban users live. ML-based moderation can run asynchronously.

  6. Are reactions shown individually?
    Typical answer

    No, as aggregated counts and an animation intensity per type.

Work out how many messages per second you would send if every one of 10 million viewers received every one of 50,000 comments per second.
Is the problem the write path or the delivery path? Which one can you make cheaper by changing what you send?
Sending the same bytes to 10 million sockets one by one from a central service will not work. How can fan-out be split into tiers?
Reactions are counts, not messages. What does that let you do with them?

Requirements

Functional

  • Post a comment or a reaction while watching a live stream.
  • Receive new comments and reaction counts for the stream being watched, in near real time.
  • Moderators delete comments and ban users; deleted comments disappear for viewers.
  • Replay: comments are stored with the video timestamp and shown in sync during playback of the recording.

Non-functional

  • Comment visible to other viewers within 2 s at p95; the author sees their own comment immediately.
  • Support 50 M concurrent viewers platform-wide and 10 M on a single stream.
  • Absorb 50 K comments/s and 500 K reactions/s on one stream without affecting other streams.
  • Comment posting available 99.9%; the video must keep playing even if comments fail.

Out of scope

  • Video ingest, transcoding and delivery.
  • Paid comments and tipping (mention as a follow-up).
  • Training moderation models.

Back-of-the-envelope estimates

  • Naive fan-out on the biggest stream: 50,000 comments/s × 10 M viewers = 500 billion deliveries per second. Impossible, and useless: nobody can read 50,000 comments a second.
  • What we actually send: about 10 comments/s per viewer plus reaction counts, batched into 2 frames per second of roughly 1 KB each. That is 20 M frames/s and about 20 GB/s (160 Gbps) of egress for the one stream, spread over the edge.
  • Connections: 50 M concurrent WebSocket or SSE connections. At ~100,000 per gateway, that is about 500 gateways, of which ~100 serve the biggest stream.
  • Writes: 50,000 comments/s × ~300 bytes = 15 MB/s into the stream and store, plus 500,000 reactions/s that are aggregated in memory and never stored one by one.
  • Storage: 100 M daily users × 5 comments × 300 bytes ≈ 150 GB/day for replay and moderation.

The delivery side drives the design: sampling and batching cut the work by four orders of magnitude, and a fan-out tree spreads what remains.

API

POST /v1/streams/{streamId}/comments   {text, clientId, videoTs}   -> 202 {commentId}
POST /v1/streams/{streamId}/reactions  {type}                      -> 202
GET  /v1/streams/{streamId}/live        (WebSocket or SSE)          -> frames:
     {seq, comments:[{id, user, text}], reactions:{heart: 41000, fire: 23500}, deletes:[ids]}
GET  /v1/streams/{streamId}/comments?fromTs=&toTs=                  -> replay page
POST /v1/streams/{streamId}/moderation  {deleteCommentId | banUserId}

Posting is a plain HTTP request that returns 202 once the comment is queued, while viewing is a one-way push channel, because the two paths scale and fail independently.

Data model

comments(stream_id, bucket_minute, comment_id, user_id, text, video_ts, status) in a wide-column store, partitioned by (stream_id, bucket_minute) so a busy stream spreads over many partitions and replay reads a time range in order. status marks deleted or hidden comments.

reaction_counts(stream_id, second, type, count): pre-aggregated per second, written by the aggregator, used for replay and the creator's graph.

bans(stream_id or channel_id, user_id, until) are cached in the Comment API for instant checks.

Subscription registry: in-memory map stream_id → set of gateway ids that currently have viewers of that stream, so the broadcast tier sends each frame once per gateway, not once per viewer.

High-level design

Posting. A viewer's comment goes through the edge load balancer to the Comment API, which authenticates the user, enforces a per-user rate limit and ban list, and runs a fast blocklist filter. Accepted comments are appended to the Comment Stream, partitioned by stream id (and sub-partitioned for the few huge streams), and the API returns 202. The author's app displays the comment locally right away. Reactions follow the same path but are pre-aggregated per API instance for 200 ms before being sent on.

Selecting. The Sampler/Aggregator consumes the stream per live stream. For every 500 ms window it sums reaction counts, removes duplicates and spam, scores comments (verified creators, accounts with history, the creator's own replies) and picks a small set to broadcast. Small streams skip sampling and pass everything through. Every accepted comment is also written to the Comment Store for replay and moderation; asynchronous ML moderation can flag comments there and issue deletes.

Delivering. The Broadcast Tier receives one frame per stream per window and looks up which Edge Gateways have viewers of that stream in the Subscription Registry. It sends one copy to each gateway, and each gateway writes the frame to its tens of thousands of local sockets. A viewer joining mid-stream gets the last few frames from the gateway's local buffer.

Open the HLD board and press Reference to see the architecture diagram.

Deep dives

Sampling: send what people can read

The display shows maybe 10 to 20 new comments per second before it becomes an unreadable blur, so that is the budget per viewer regardless of how many arrive. The sampler keeps a per-stream window, drops duplicate text (floods of the same chant or spam), and ranks the rest by a cheap quality score. Everyone receives the same globally sampled frame, which keeps the system simple and gives a shared experience. Two personal additions are merged on the device or gateway: the viewer's own comments, and optionally comments from people they follow. When a stream is small, the sampler is a pass-through. The budget can adapt: if fewer comments arrive than the budget, everything is shown.

Fan-out tree to millions of sockets

A single service cannot write 20 M frames per second. Split delivery into tiers. The broadcast tier sends each frame once per gateway that has subscribers, so 100 gateways for the biggest stream rather than 10 M viewers. Each gateway then writes to its own 100,000 local connections, which is CPU-bound work that scales horizontally. For even larger events, add a relay layer between broadcast and gateways, organised by region, so the core sends once per region. Gateways subscribe to a stream when the first local viewer joins and unsubscribe when the last leaves. Frames carry a sequence number so a gateway can detect a gap and fetch the missed frame.

Reactions as counters

Reactions are the biggest write volume but the easiest to compress. Each Comment API instance keeps in-memory counts per stream and type and flushes them every 200 ms; the aggregator sums the partial counts and includes totals in the next frame. Viewers see an animation scaled by the rate, not individual taps, so losing a few hundred taps in a crash is invisible. Per-second totals are stored for replay. Because the aggregation is associative, it can be layered: instance, then region, then global.

Moderation and deletes in a live stream

Cheap checks (rate limits, bans, blocklists, link detection) run synchronously in the Comment API so blatant abuse never enters the stream. Slower ML classification runs asynchronously on the stored comment; if it flags a comment, or a moderator deletes one, a delete event goes into the next frame and clients remove it if it is still on screen. Replay reads honour the status column, so deleted comments never resurface. A ban is pushed to every API instance's cache within seconds.

Failure modes

  • If an edge gateway dies, its viewers reconnect to another one, which subscribes to the stream and replays its recent frame buffer; the video is unaffected.
  • If the sampler for a stream falls behind, it skips ahead to the newest window rather than catching up, because stale comments are worthless in a live context.
  • If the Comment Store is slow, live delivery continues from the stream and persistence catches up later; replay data arrives late but is not lost.
  • If one huge stream overloads its stream partitions, per-stream quotas and separate partitions keep other streams unaffected.

Core algorithm in code

from collections import Counter


def quality(c):
    """Cheap per-comment score: prefer verified or established accounts and real text."""
    s = 2.0 if c["verified"] else 0.0
    s += min(c["followers"], 10_000) / 10_000
    words = len(c["text"].split())
    s += 0.5 if 2 <= words <= 30 else -1.0          # skip one-word spam and walls of text
    return s


def build_frame(comments, reactions, k=3):
    """One broadcast frame for a 500 ms window of a busy stream.
    comments: [{"user", "text", "followers", "verified"}], reactions: ["heart", "laugh", ...]
    Return (up to k chosen comments, reaction counts, total comment count)."""
    seen_text, unique = set(), []
    for c in comments:
        key = c["text"].lower().strip()
        if key in seen_text:                        # copy-paste floods collapse to one
            continue
        seen_text.add(key)
        unique.append(c)
    chosen = sorted(unique, key=quality, reverse=True)[:k]
    return chosen, Counter(reactions), len(comments)


window = [
    {"user": "fan01", "text": "GOAL!!!", "followers": 12, "verified": False},
    {"user": "fan02", "text": "what a strike from outside the box", "followers": 340, "verified": False},
    {"user": "club", "text": "Unbelievable finish, 2-1 with ten minutes left", "followers": 900_000, "verified": True},
    {"user": "bot77", "text": "free coins at my profile", "followers": 0, "verified": False},
    {"user": "bot78", "text": "free coins at my profile", "followers": 0, "verified": False},
    {"user": "fan03", "text": "keeper had no chance on that one", "followers": 5_200, "verified": False},
]
reactions = ["heart"] * 41_000 + ["fire"] * 23_500 + ["laugh"] * 900

chosen, counts, total = build_frame(window, reactions)
print(f"{total} comments in window, showing {len(chosen)}:")
for c in chosen:
    print(f"  @{c['user']}: {c['text']}")
print("reaction totals:", dict(counts.most_common()))

Trade-offs

  • Global sampling keeps delivery cost flat and gives everyone the same view, but costs completeness: most comments on a huge stream are never shown to anyone except their author.
  • Batching into 500 ms frames cuts per-message overhead dramatically but adds up to half a second of latency.
  • Server-Sent Events are simpler and proxy-friendly for one-way delivery, while WebSockets cost more complexity but carry posts and reactions on the same connection.
  • Pre-aggregating reactions in memory scales to millions of taps but accepts losing a fraction of a second of counts if an instance crashes.
  • A fan-out tree spreads the load but adds a hop and more components that can fall behind.

What a strong answer shows

  • Does the naive fan-out arithmetic and concludes that sampling and batching are required.
  • Separates the write path (HTTP, queue) from the delivery path (persistent push connections).
  • Designs tiered fan-out: send once per gateway or region, not once per viewer.
  • Treats reactions as aggregated counters rather than individual messages.
  • Handles moderation both inline and asynchronously, including deletes in flight and in replay.
  • Explains degradation: comments may lag or drop, but the video always plays.

Mistakes to avoid

  • Promising every viewer every comment and trying to scale the impossible version.
  • Storing and broadcasting each reaction tap as its own message.
  • Routing all viewers of a stream through one server or one pub/sub topic consumer.
  • Ignoring the author's own comment, which must appear instantly even if it is not sampled for others.
  • Forgetting that replay needs comments stored with the video timestamp.

Follow-ups to expect

  • How would you add paid highlighted comments? (Separate priority lane that bypasses sampling; payment confirmed before broadcast.)
  • How would you show comments from friends on a huge stream? (Per-viewer merge at the gateway from a small friends' comment channel.)
  • How would you support many languages? (Optionally translate or filter by language at the sampler, producing a few frame variants per stream.)
  • How do you keep comment timing in sync with video that is delayed 20 s by the player buffer? (Tag comments with video timestamp and let the client delay display to match.)

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url