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.
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.