Live Comments System Low-Level Design

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.

Scroll to Top