JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionssystem-design
Prev

Learn the concept

Real-Time System Architecture

system-design
senior
chat

Design a real-time chat application like Slack or Messenger

chat
websocket
real-time
optimistic-ui
typing-indicator
virtualization
read-receipts
file-upload
Quick Answer

A real-time chat application uses WebSocket connections with heartbeat and exponential backoff reconnection for message delivery, optimistic UI for instant message display, virtualized message lists for performance, and features like typing indicators (debounced broadcasts), read receipts (intersection observer), and file uploads (presigned URLs with progress tracking).

Detailed Explanation

Designing a chat application is one of the most comprehensive frontend system design questions because it touches nearly every aspect of modern web development: real-time communication, state management, performance optimization, accessibility, and complex UI interactions.

Requirements

Functional:

  • 1:1 and group conversations
  • Text messages with markdown support
  • Message history with infinite scroll (load older messages)
  • Typing indicators ("Alice is typing...")
  • Read receipts (seen/delivered)
  • File and image sharing
  • Emoji reactions on messages
  • Thread replies
  • User presence (online/offline/away)
  • Search across messages

Non-functional:

  • Messages appear within 100ms of sending (optimistic UI)
  • Support conversations with 100K+ messages (virtualization)
  • Work on slow networks (offline queue, retry)
  • Accessible to screen reader users
  • Handle 100+ concurrent conversations in the sidebar

WebSocket Architecture

WebSocket is the correct transport for chat because it provides full-duplex, low-latency communication. The connection lifecycle requires careful management:

Connection Management

  1. Initial connection — Connect when the app loads. Authenticate via a token in the first message or as a query parameter.
  2. Heartbeat — Send a ping every 30 seconds. If no pong is received within 5 seconds, consider the connection dead and initiate reconnection.
  3. Reconnection — Use exponential backoff with jitter: delay = min(baseDelay * 2^attempt + randomJitter, maxDelay). This prevents a thundering herd when a server restarts and thousands of clients reconnect simultaneously.
  4. Message queuing — While disconnected, queue outgoing messages locally. Replay them in order when the connection is restored.
  5. Multiplexing — A single WebSocket connection handles all conversations. Messages include a channelId to route them to the correct conversation.

Message Data Model

Message {
  id: string (UUID or server-generated snowflake)
  channelId: string
  senderId: string
  content: string
  type: 'text' | 'image' | 'file' | 'system'
  threadId: string | null (for replies)
  reactions: Map<emoji, userId[]>
  createdAt: timestamp
  updatedAt: timestamp | null
  status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
  replyTo: string | null (quoted message ID)
}

Use client-generated UUIDs for message IDs. This enables optimistic UI — the message can be rendered immediately with a temporary ID that becomes permanent when the server confirms.

State Management

Chat state is complex. Organize it into domains:

  1. Conversations — List of channels/DMs with last message preview, unread count, pinned status.
  2. Messages — Per-channel message arrays. Only keep recent messages in memory; older ones are fetched on scroll.
  3. Users — User profiles, presence status (online/offline/away/DND), typing state.
  4. Connection — WebSocket state, pending message queue, reconnection status.

Use normalized state (messages stored by ID in a map, channels reference message IDs) to avoid duplication and enable efficient updates when a message is edited or reacted to.

Optimistic Message Sending

When the user presses Send:

  1. Generate a client-side UUID for the message.
  2. Immediately add the message to the local message list with status: 'sending'.
  3. Send via WebSocket.
  4. When the server acknowledges (returns the server-assigned timestamp and confirms the ID), update status: 'sent'.
  5. If sending fails (timeout or error), update status: 'failed' and show a retry button.

This makes the app feel instant even on slow connections. The key is having a unique client ID so the server's confirmation can be matched to the optimistic message.

Message List Virtualization

Chat conversations can contain hundreds of thousands of messages. Rendering them all would crash the browser. Use virtualization to render only the visible messages plus a small overscan buffer.

  • Use a library like react-window or @tanstack/virtual for variable-height virtualization.
  • Messages have variable heights (text vs. images vs. files), so you need a dynamic size measurement system.
  • Maintain scroll position when new messages are loaded above (prepended). This requires calculating the height difference and adjusting scrollTop.
  • Auto-scroll to bottom when a new message arrives, but only if the user is already at the bottom. If they have scrolled up to read history, show a "New messages" badge instead.

Pagination — Cursor-Based

Use cursor-based pagination (not offset-based) for loading older messages:

  • Request: GET /api/channels/{id}/messages?before={oldestMessageId}&limit=50
  • The cursor is the ID of the oldest message currently loaded.
  • Cursor pagination is stable even when new messages are being added in real time (offset pagination would shift).

Typing Indicators

Typing indicators require balancing responsiveness with network efficiency:

  1. When the user starts typing, broadcast a typing_start event to the channel.
  2. Debounce the broadcast — do not send on every keystroke. Send once when typing begins, then suppress for 3 seconds.
  3. Set a timeout on the receiver side — if no new typing event arrives within 5 seconds, remove the indicator. This handles cases where the user stops typing without sending.
  4. Display: "Alice is typing..." for one user, "Alice and Bob are typing..." for two, "Several people are typing..." for three or more.

Read Receipts

Use the Intersection Observer API to detect which messages are visible in the viewport:

  1. Observe each unread message element.
  2. When a message becomes visible for more than 1 second (to avoid counting quick scrolls), mark it as read.
  3. Batch read receipts — send a single "read up to message ID X" event rather than individual read events for each message.
  4. Display read status with subtle indicators (checkmarks, small avatars).

File Upload

Use presigned URLs for file uploads to avoid routing large files through your WebSocket or API server:

  1. Client requests a presigned upload URL from the API.
  2. Client uploads directly to the storage service (S3, GCS) with progress tracking via XMLHttpRequest or fetch with a ReadableStream.
  3. Client sends a message with the file URL and metadata (name, size, type, dimensions for images).
  4. Generate thumbnail previews client-side using <canvas> before the upload completes for instant visual feedback.
  5. Show upload progress in the message bubble.

Search

Implement a two-tier search strategy:

  • Client-side — Search recent messages (last 1000 per channel) using a simple text match. This is instant and covers the common case.
  • Server-side — Full-text search across all messages using Elasticsearch or PostgreSQL full-text search. Triggered when the user explicitly searches or scrolls past client-side results.

Accessibility

  • Use an ARIA live region (aria-live="polite") for new incoming messages so screen readers announce them.
  • Provide keyboard navigation between channels (arrow keys in the sidebar) and within messages.
  • Ensure the message input has proper labeling and supports keyboard shortcuts (Enter to send, Shift+Enter for newline).
  • Typing indicators and presence dots need text alternatives.
  • Emoji reactions should be keyboard-accessible and have descriptive labels ("3 people reacted with thumbs up").

Code Examples

WebSocket connection manager with heartbeat and reconnectionTypeScript
type MessageHandler = (data: any) => void;

interface ChatSocketOptions {
  url: string;
  token: string;
  onMessage: MessageHandler;
  onStatusChange: (status: 'connecting' | 'connected' | 'disconnected' | 'reconnecting') => void;
}

class ChatSocketManager {
  private ws: WebSocket | null = null;
  private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
  private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
  private reconnectAttempt = 0;
  private maxReconnectAttempt = 15;
  private messageQueue: any[] = [];
  private isIntentionallyClosed = false;

  constructor(private options: ChatSocketOptions) {}

  connect(): void {
    this.isIntentionallyClosed = false;
    this.options.onStatusChange('connecting');

    this.ws = new WebSocket(`${this.options.url}?token=${this.options.token}`);

    this.ws.onopen = () => {
      this.options.onStatusChange('connected');
      this.reconnectAttempt = 0;
      this.startHeartbeat();
      this.flushQueue();
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);

      // Heartbeat response — reset the timeout
      if (data.type === 'pong') return;

      this.options.onMessage(data);
    };

    this.ws.onclose = (event) => {
      this.stopHeartbeat();
      this.options.onStatusChange('disconnected');

      if (!this.isIntentionallyClosed && event.code !== 1000) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = () => {
      // onerror is always followed by onclose, so reconnection
      // is handled there. Just log for debugging.
    };
  }

  send(data: any): void {
    const message = JSON.stringify(data);

    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      // Queue for later delivery
      this.messageQueue.push(data);
    }
  }

  disconnect(): void {
    this.isIntentionallyClosed = true;
    this.stopHeartbeat();
    clearTimeout(this.reconnectTimeout!);
    this.ws?.close(1000, 'User disconnected');
  }

  private startHeartbeat(): void {
    this.heartbeatInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));

        // If no pong within 5 seconds, connection is dead
        const pongTimeout = setTimeout(() => {
          this.ws?.close(4000, 'Heartbeat timeout');
        }, 5000);

        // Clear the timeout when we receive any message
        // (the onmessage handler above handles pong)
        const originalHandler = this.ws.onmessage;
        this.ws.onmessage = (event) => {
          clearTimeout(pongTimeout);
          this.ws!.onmessage = originalHandler;
          originalHandler?.call(this.ws, event);
        };
      }
    }, 30000);
  }

  private stopHeartbeat(): void {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  // Exponential backoff with jitter to prevent thundering herd
  private scheduleReconnect(): void {
    if (this.reconnectAttempt >= this.maxReconnectAttempt) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.options.onStatusChange('reconnecting');

    const baseDelay = 1000;
    const maxDelay = 30000;
    const exponentialDelay = baseDelay * Math.pow(2, this.reconnectAttempt);
    const jitter = Math.random() * 1000;
    const delay = Math.min(exponentialDelay + jitter, maxDelay);

    this.reconnectTimeout = setTimeout(() => {
      this.reconnectAttempt++;
      this.connect();
    }, delay);
  }

  // Replay queued messages after reconnection
  private flushQueue(): void {
    while (this.messageQueue.length > 0) {
      const data = this.messageQueue.shift();
      this.send(data);
    }
  }
}

Real-World Applications

Use Cases

Team Communication Platform

Building a Slack-like workspace with channels, direct messages, threads, and integrations, requiring real-time message delivery across thousands of concurrent users

Customer Support Chat Widget

Embedding a chat widget on a website where customers can communicate with support agents in real time, with typing indicators and file sharing for screenshots

In-App Messaging for Social Platform

Adding direct messaging to a social media application, with read receipts, online presence, and media sharing, handling millions of concurrent conversations

Mini Projects

Real-Time Chat Room

advanced

Build a multi-room chat application with WebSocket connections, optimistic message sending, typing indicators, and message history with cursor-based pagination

Chat Message Virtualization

advanced

Create a virtualized message list that handles variable-height messages, maintains scroll position when loading history, and auto-scrolls on new messages

Industry Examples

Slack

Uses WebSocket connections for real-time message delivery with sophisticated reconnection logic, message threading, and presence indicators across millions of workspaces

Discord

Handles millions of concurrent WebSocket connections with gateway sharding, optimistic UI for instant message display, and efficient binary encoding for reduced bandwidth

WhatsApp Web

Uses WebSocket connections for real-time messaging with end-to-end encryption. Since 2021, multi-device support allows up to 4 linked devices to connect independently without requiring the phone to be online

Resources

MDN — WebSocket API

docs

Socket.IO Documentation

docs

RFC 6455 — The WebSocket Protocol

docs

MDN — Intersection Observer API

docs

Related Questions

Design a real-time notification system for a web application

mid
real-time

Design an autocomplete/typeahead search component

mid
autocomplete

How do you design a component library / design system for multiple teams?

mid
component-architecture

What is the difference between client state and server state?

junior
data-layer
Previous
Design an offline-first web application with sync capabilities
Prev