Live Comments
Design a live commenting system for broadcasts like Facebook Live or YouTube Live that delivers thousands of new comments per second to millions of concurrent viewers in near real time.
What is a live comments feature?
A live comments feature lets viewers post and see comments on a broadcast in real time. The interesting engineering problem is not only storage; it is fan-out. One comment must reach the many gateway nodes holding viewer connections without making the comment writer wait for every socket.
The design separates durable comment ingestion, recent-history reads, and live delivery. A durable event stream feeds a stream-aware fan-out layer, while a dedicated WebSocket gateway owns connection state. The numbers below make the fan-out cost visible, but the architecture remains valid when the workload is smaller.
TL;DR
Persist a comment through a Comment Service and publish a keyed event to a durable broker. Shard WebSocket connections by stream_id across a dedicated gateway fleet; fan-out workers push each event only to the gateway nodes serving that stream, and each gateway broadcasts to its local connections. Use a bounded recent-comments cache with a database fallback for join-time history.
Delivery is at-least-once and approximately ordered. Clients deduplicate by comment_id, reconnect with a cursor or fetch recent history after a disconnect, and receive an explicit slow-consumer or dropped-event signal when they cannot keep up. Moderation, long-term threads, and cross-region active-active delivery are separate extensions.
Scope and assumptions
This article uses an illustrative single-region baseline with an optional regional edge-relay extension:
- A popular stream can have up to 10 million concurrent viewers and 10,000 new comments/second during a short peak. The resulting 100 billion logical deliveries/second is a fan-out obligation, not a claim that the system sends that many independent broker messages.
- A comment should appear within 1 second at the stated target, with approximate order within a stream and at-least-once delivery acceptable. Exact-once delivery is not required.
- Viewers load the latest 100 comments on join. The canonical comment store retains more history than the hot cache, while the Redis recent set is bounded.
- WebSocket is the primary bidirectional protocol. SSE is a valid read-only alternative, and long polling is a fallback for constrained clients.
- The baseline covers one region and one stream's connection shard group. Moderation, post-stream threads, reactions, and global cross-region replication are below the line.
Functional Requirements
Core Requirements
- Viewers can post comments on a live stream.
- All viewers see new comments within 1 second of posting.
- Popular streams may have millions of concurrent viewers and thousands of comments per second.
- Viewers can load the most recent comments when they first join a stream.
Below the Line (out of scope)
- Comment moderation and toxicity filtering
- Persistent comment threads after the stream ends
- Reactions and emoji bursts
The hardest part in scope: Fan-out. One comment post triggers delivery to potentially millions of open WebSocket connections simultaneously. Doing this with a naive shared server creates a single point of failure when traffic is highest.
Comment moderation is below the line because it does not change the delivery path. To add it, attach an async Kafka consumer that runs each new comment through a content classifier. Shadow-delete flagged comments before they reach the fan-out layer. An admin UI exposes the shadow-deleted queue for human review.
Persistent threads are below the line because they live on a different access pattern: paginated reads against a stable dataset rather than streaming pushes. To add them, flush the stream's comment log to a relational comments table on stream-end and serve it through a standard paginated API.
Reactions are below the line because they require a separate aggregation strategy: individual reaction events must be collapsed to per-stream emoji counters before delivery. To add them, route reaction events through a sliding-window counter service and deliver aggregate counts to viewers on a lower-frequency channel (every 500ms rather than per event).
Non-Functional Requirements
Core Requirements
- Latency: A posted comment appears on all viewer screens within 1 second of posting (p99). This rules out polling as the delivery mechanism, since polling at 1-second intervals burns N requests per second where N is the number of viewers.
- Availability: 99.99% uptime. New comments must be postable even during partial backend failures. Availability over consistency: if a viewer misses one comment during a brief partition, that is acceptable.
- Scale: 10M concurrent viewers per stream at peak; up to 10K comments per second during high-energy moments. These numbers must not be treated as theoretical.
- Ordering: Comments within a stream appear in approximate posting order. Eventual consistency is acceptable for comments posted within the same second.
- Durability: Recently posted comments survive a server restart. A viewer joining the stream sees the last 100 comments.
Below the Line
- Exactly-once comment delivery (at-least-once is fine; duplicate suppression adds coordination cost without user-visible benefit)
- Cross-region synchronization for global broadcasts (viable through multi-region Kafka replication but deferred)
Write/fan-out ratio: With 10K comments per second and 10M concurrent viewers, each write triggers 10M deliveries. That is a 1:10,000,000 write-to-fan-out amplification. This amplification dominates capacity planning for the design, so use it to anchor the interview discussion.
The fan-out ratio means the logical delivery obligation is 10,000 Γ 10,000,000 = 100 billion viewer deliveries per second at the illustrative peak. The system does not send 100 billion independent broker messages: shared subscriptions, stream-sharded gateways, and regional relays reduce internal duplication. The logical fan-out is still the capacity constraint.
30-second answer / outline
- Accept and validate comments in a stateless Comment Service; write the canonical record and publish an event keyed by
stream_id. - Route WebSocket connections to a dedicated, stream-sharded gateway fleet. Keep connection registries local to gateway nodes.
- Use fan-out workers and a gateway registry to deliver each event to the small gateway group for that stream, then broadcast locally.
- Keep the latest 100β200 comments in a bounded Redis sorted set and fall back to the canonical store on cache miss or reconnect.
- Define failure behavior: at-least-once events, client deduplication, slow-consumer backpressure, reconnect cursors, and approximate ordering.
5-minute explanation
There are three different paths. The write path validates and persists a comment, then publishes one event. The delivery path consumes that event and sends it to the gateway shard group for the stream; the gateway performs the unavoidable local O(number of connected viewers on that node) socket writes. The join path reads recent history from Redis and uses the database only as a cold fallback.
WebSockets keep posting and receiving on one bidirectional connection, but SSE is simpler when viewers only read. Redis Pub/Sub is easy but loses events during disconnects, so the recommended baseline uses a retained broker or event log for recovery. A viewer reconnect should use a last_seen_id or timestamp cursor and replay from recent history; a gateway's internal broker offset is not a per-viewer delivery guarantee.
At mega-stream scale, grouping connections by stream_id limits each comment's internal push to the nodes serving that stream. Regional edge relays can reduce last-mile distance, but they add another stateful tier and need their own backpressure, authentication, and reconnect behavior. Monitor event lag, connection counts, send queues, dropped messages, and history-cache misses.
45-minute interview approach
This is a time-boxed interview plan, not a promise that the article can or should be read in 45 minutes.
- 0β5 minutes β Clarify the contract: Confirm comment rate, concurrent viewers per stream, delivery latency, ordering, replay, moderation, retention, and whether viewers must post on the same connection.
- 5β10 minutes β Establish scale: Compute the logical fan-out, distinguish broker events from socket deliveries, and separate ordinary streams from mega-streams.
- 10β15 minutes β Define APIs and invariants: Show post, recent-history, WebSocket join, cursor/reconnect,
comment_iddeduplication, and authorization rules. - 15β22 minutes β Draw ingestion and history: Start with the database plus recent cache, then add the durable broker and explain why the writer does not synchronously broadcast.
- 22β30 minutes β Draw live delivery: Add the WebSocket gateway fleet, stream-shard registry, fan-out workers, local subscriber maps, and slow-consumer policy.
- 30β36 minutes β Compare protocols and pub-sub: Contrast long polling, SSE, Redis Pub/Sub, and a retained event stream; state the chosen delivery and replay semantics.
- 36β42 minutes β Reliability, security, and operations: Cover gateway restarts, reconnects, duplicate events, backpressure, moderation hooks, connection limits, authentication, and regional failover.
- 42β45 minutes β Trade-offs and close: Explain when edge relays or a simpler SSE design are enough, recap the fan-out bottleneck, and invite follow-up questions.
Core Entities
- Stream: The live broadcast. Identified by a
stream_id, carries the host user, creation timestamp, and status (liveorended). All other entities link to a stream. - Comment: A single comment event. Carries a
comment_id,stream_id,author_id, text content, andposted_attimestamp. The primary unit of storage and delivery. - ViewerSession: A connected viewer. Carries a
viewer_id,stream_id, connection metadata (server node, socket ID), and join timestamp. Lives as long as the WebSocket connection is open.
Full schema details, including indexes and the Redis sorted set shape, are deferred to the deep dives. These three entities are sufficient to drive the API and High-Level Design.
API Design
The API has two distinct shapes: a REST endpoint for posting comments, and a streaming channel for receiving them. Both are needed because the client is a participant (posting) and a subscriber (receiving).
FR 1 and FR 4: Post a comment and join a stream:
POST /streams/{stream_id}/comments
Authorization: Bearer <token>
Body: { text }
Response: { comment_id, posted_at }
POST creates a new resource. The response returns only the server-assigned comment_id and timestamp; the other fields are known to the client already. Authentication is not expanded here, but the endpoint should accept a standard Authorization: Bearer credential and enforce stream permissions.
FR 2: Open a real-time comment stream:
WebSocket: wss://host/streams/{stream_id}/feed
Server pushes: { comment_id, author_id, text, posted_at }
WebSocket is preferable to Server-Sent Events (SSE) here because viewers both post and receive. SSE is unidirectional: great for read-only consumption, but it requires a separate POST endpoint for writes and two separate connections per client. WebSocket gives you a single bidirectional connection per viewer, which halves the connection count and simplifies multiplexing comments and system messages (like "Stream is ending") over the same channel.
Long polling is not a serious option: it works for small audiences but generates N reconnect storms per second and cannot hit the 1-second latency target at scale.
FR 4: Load recent comments on join:
GET /streams/{stream_id}/comments/recent?limit=100
Response: { comments: [{ comment_id, author_id, text, posted_at }], oldest_cursor }
The client calls this once when joining, before or coincident with the WebSocket handshake. The oldest_cursor is a timestamp-based cursor for paging further back if the viewer wants scrollback. I'd design it cursor-based rather than offset-based because the comment stream is append-only; cursor pagination is stable under concurrent inserts.
Keep history and streaming separate
Some designs use the WebSocket connection itself to replay recent comments on open. This works but couples the replay mechanism to the real-time delivery path. Keep them separate: REST for history, WebSocket for live.
High-Level Design
Critical flows
- Post: Authenticate and validate the comment, persist it, and publish one event keyed by
stream_id. - Deliver: Fan-out workers resolve the stream's active gateway nodes; each gateway sends the event to its local viewer connections and tracks slow consumers.
- Join/reconnect: Load recent history from the bounded cache or database fallback, then attach the live connection with a cursor so the client can deduplicate any overlap.
- Failure: Reconnect gateways and clients with backoff, replay recent history by cursor, and surface broker lag or dropped events rather than claiming exactly-once delivery.
1. Posting and loading comments (FR 1 and FR 4)
The write and read path first, real-time delivery later.
Start with the simplest possible system: the client posts a comment, an App Server stores it, and the same server serves recent comments when a new viewer joins.
Components:
- Client: Web or mobile app sending POST requests and WebSocket upgrades.
- App Server: Receives comment POSTs, validates them, writes to the database, and serves recent-comment GETs.
- Database: Stores all Comment rows. Indexed by
(stream_id, posted_at DESC)for efficient recent-comment queries.
Request walkthrough (post a comment):
- Client sends
POST /streams/{stream_id}/commentswith{ text }. - App Server validates the request (stream exists, text not empty).
- App Server generates a
comment_idand writes the comment to the database. - App Server returns
{ comment_id, posted_at }to the client.
Request walkthrough (load recent comments on join):
- Client sends
GET /streams/{stream_id}/comments/recent?limit=100. - App Server queries the database for the 100 most recent comments on this stream.
- App Server returns the sorted list to the client.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.