Live Comments System — Low-Level Design
A live comments system streams new comments to all viewers of a piece of content in real time — without page refresh. This pattern powers live event chats, YouTube live comments, and collaborative document annotations. The core challenges are connection management at scale and fan-out to thousands of concurrent viewers.
Connection Architecture Options
Option 1: Long-polling
Client polls GET /comments?since={last_id} every 2 seconds.
Simple to implement. High server load (many idle connections).
Latency: up to 2 seconds. Works everywhere (no special infrastructure).
Option 2: Server-Sent Events (SSE)
Client opens a persistent GET connection.
Server pushes new comments as text/event-stream.
One-way (server → client). Automatic reconnect built into browser.
Good for comment feeds (client never sends data on the stream).
Option 3: WebSocket
Bidirectional. Client can send and receive on the same connection.
More complex to manage (connection state, heartbeats, reconnect).
Best for: chat, collaborative editing where client sends events back.
Recommendation: SSE for live comment feeds (simpler, sufficient).
WebSocket only if you need client → server streaming (live typing indicators, etc.)
Core Data Model
Comment (same as threaded comments, with additions)
id BIGSERIAL PK
content_id BIGINT NOT NULL
author_id BIGINT NOT NULL
body TEXT NOT NULL
created_at TIMESTAMPTZ NOT NULL
-- Index for incremental polling: fast since-id queries
CREATE INDEX idx_live_comments ON Comment(content_id, id ASC);
SSE Endpoint Implementation
from flask import Response, stream_with_context
import time
def live_comments_stream(content_id):
def event_stream():
last_id = get_latest_comment_id(content_id)
while True:
# Poll for new comments since last seen
new_comments = db.execute("""
SELECT id, author_id, body, created_at
FROM Comment
WHERE content_id=%(cid)s AND id > %(last_id)s
ORDER BY id ASC
LIMIT 50
""", {'cid': content_id, 'last_id': last_id})
for comment in new_comments:
last_id = comment.id
data = json.dumps({
'id': comment.id,
'author': comment.author_id,
'body': comment.body,
'created_at': comment.created_at.isoformat()
})
# SSE format: "data: {json}nn"
yield f'id: {comment.id}ndata: {data}nn'
time.sleep(1) # Poll interval
return Response(
stream_with_context(event_stream()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no', # Disable nginx buffering
}
)
Scaling with Redis Pub/Sub
# DB polling (above) doesn't scale: N connected viewers = N DB queries/second
# Solution: publish new comments to Redis, SSE workers subscribe
def post_comment(content_id, author_id, body):
comment = db.insert(Comment, {
'content_id': content_id,
'author_id': author_id,
'body': body,
})
# Publish to Redis channel for this content
redis.publish(f'comments:{content_id}', json.dumps({
'id': comment.id,
'author_id': author_id,
'body': body,
'created_at': comment.created_at.isoformat(),
}))
return comment
def live_comments_stream_redis(content_id):
def event_stream():
pubsub = redis.pubsub()
pubsub.subscribe(f'comments:{content_id}')
for message in pubsub.listen():
if message['type'] == 'message':
yield f'data: {message["data"].decode()}nn'
return Response(stream_with_context(event_stream()), mimetype='text/event-stream')
Fan-Out at Scale
Scale challenge:
1,000 viewers on a live event → 1,000 SSE connections per server
With 10 app servers: 100 SSE connections per server on average
A new comment must reach all 10 servers → each server's 100 connections get it
Solution: Redis Pub/Sub fan-out
App server 1: handles 100 SSE connections, subscribed to channel 'comments:456'
App server 2: handles 100 SSE connections, subscribed to channel 'comments:456'
...
New comment published to 'comments:456' → Redis delivers to ALL subscribed servers
Each server pushes to its local connections
This works for thousands of viewers.
For millions of viewers (major live events):
Use a dedicated real-time messaging service (Pusher, Ably, Firebase Realtime DB)
or a persistent message broker with partition-based fan-out (Kafka → streaming nodes).
Client Reconnection and Missed Comments
// JavaScript client with automatic reconnection
const evtSource = new EventSource(`/stream/comments/${contentId}`);
let lastEventId = null;
evtSource.onmessage = (event) => {
lastEventId = event.lastEventId; // Browser tracks this automatically
displayComment(JSON.parse(event.data));
};
evtSource.onerror = () => {
// Browser auto-reconnects; sends Last-Event-ID header on reconnect
// Server should use this header to replay missed comments
};
// Server: on reconnect with Last-Event-ID, replay missed comments
def event_stream_with_replay(content_id, last_event_id):
if last_event_id:
# Send missed comments first
missed = db.query("""
SELECT * FROM Comment
WHERE content_id=%(cid)s AND id > %(lid)s
ORDER BY id ASC
""", {'cid': content_id, 'lid': int(last_event_id)})
for c in missed:
yield f'id: {c.id}ndata: {json.dumps(c)}nn'
# Then subscribe to live stream
Key Interview Points
- SSE over WebSocket for unidirectional feeds: SSE is simpler (standard HTTP, automatic reconnect, no special library), sufficient for read-only comment streams, and works through HTTP/2 multiplexing.
- Redis Pub/Sub eliminates per-viewer DB polling: Without Redis, N viewers = N queries/second. With Redis, all app servers subscribe to one channel — one Redis message delivers to all.
- Last-Event-ID for gap recovery: SSE sends a Last-Event-ID header on reconnect. Use it to replay missed comments rather than having the client reload the page.
- Connection limits: Each SSE connection is an open HTTP connection. A Node.js or async Python server can hold tens of thousands of idle connections. A threaded server (traditional Rails, Django) can only hold as many connections as it has threads.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”Why use Server-Sent Events instead of WebSocket for a live comment feed?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”SSE is a one-way (server → client) protocol over plain HTTP. It uses the browser’s built-in EventSource API which handles reconnection automatically with the Last-Event-ID header. SSE works through standard HTTP load balancers and proxies without special configuration. WebSocket is bidirectional — use it when clients need to send data back on the same connection (live typing, collaborative editing). For a read-only comment stream where the user only submits comments via regular POST requests, SSE is simpler, uses less memory per connection, and has automatic reconnect built in.”}},{“@type”:”Question”,”name”:”How does Redis Pub/Sub eliminate the N×M polling problem?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Without Redis: each of M app servers polls the database every second for new comments on each active content item. With N content items and M servers, this is N×M queries/second — a database-killing load for popular live events. With Redis Pub/Sub: when a new comment is posted, publish one message to a Redis channel (comments:{content_id}). Each app server subscribes to only the channels for content their connected SSE clients are watching. Redis delivers the message to all subscribed servers in one operation. The database load becomes one write per new comment rather than N×M reads per second.”}},{“@type”:”Question”,”name”:”How does the Last-Event-ID header enable comment gap recovery?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Every SSE message includes an id field (id: {comment_id}). The browser stores the last received id in memory. On reconnection (network drop, server restart), the browser automatically sends the Last-Event-ID header with this stored id. The server queries: SELECT * FROM Comment WHERE content_id=X AND id > last_event_id ORDER BY id to replay any comments received during the disconnection gap. Without this mechanism, users who experience a brief network interruption would silently miss comments that arrived while they were disconnected.”}},{“@type”:”Question”,”name”:”How do you limit live comment load for extremely popular events?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”At 100,000 concurrent viewers, a new comment every second generates 100,000 SSE writes/second across all connections. Strategies: (1) Throttle: batch comments and send updates every 500ms instead of per-comment — reduces writes to 200K/sec×50%=2x savings. (2) Sample: at very high rates, only show a subset of comments ("showing every 10th comment during peak traffic"). (3) Separate hot event infrastructure: route high-traffic events to a dedicated real-time service (Pusher, Ably) that is designed for this workload. (4) Use WebSocket multiplexing — one connection per server handling thousands of clients.”}},{“@type”:”Question”,”name”:”How do you prevent spam in live comment streams?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Rate limit comment submission per user: max 3 comments per 10 seconds using Redis INCR with TTL. Require authentication for live comment submission — anonymous posting is the main spam vector. Filter known spam patterns server-side before inserting. Queue comments through a fast keyword filter before fan-out: exact-match block lists take microseconds. For high-profile events: implement a short moderation queue (5-second hold before display) so moderators can remove spam before it reaches viewers. Display "sent" confirmation to the poster immediately but delay actual fan-out.”}}]}
Live comments and real-time feed design is discussed in Twitter system design interview questions.
Live comments and SSE streaming design is covered in Meta system design interview preparation.
Live comments and real-time streaming system design is discussed in Google system design interview guide.