AboutBlogContact
Web DevelopmentApril 5, 2026 5 min read 102Updated: June 22, 2026

How to Build a Real-Time Chat Application: Architecture Deep Dive

AunimedaAunimeda
📋 Table of Contents

Real-time chat looks deceptively simple. In production, you need: message delivery guarantees, offline message queuing, read receipts, typing indicators, message history pagination, and a system that handles network interruptions gracefully. Here's the full picture.


Transport Layer: WebSocket vs SSE vs Polling

WebSocket: Persistent bidirectional connection. Server can push to client anytime. Best for chat.

Server-Sent Events (SSE): Server-to-client only. Simpler than WebSocket, works over HTTP/2. Good for notifications, not full chat.

Long Polling: HTTP request held open until server has data. Legacy approach. Works everywhere but inefficient.

For chat: WebSocket is the answer. Bidirectional, low overhead, wide browser/mobile support.


Core Architecture

Client App ←→ WebSocket Server ←→ Message Queue ←→ Database
                    ↓
              Presence Service
                    ↓
           Push Notification Service

Why a message queue?
Direct DB writes on every message create write bottlenecks. Queue absorbs spikes, guarantees delivery order, enables fan-out (broadcasting to all room members).


WebSocket Server with Node.js

import { WebSocketServer } from 'ws';
import { createClient } from 'redis';

const wss = new WebSocketServer({ port: 8080 });
const clients = new Map(); // userId → WebSocket

wss.on('connection', (ws, req) => {
  const userId = authenticateConnection(req); // JWT from query param or header
  clients.set(userId, ws);
  
  ws.on('message', async (data) => {
    const message = JSON.parse(data);
    
    switch (message.type) {
      case 'send_message':
        await handleSendMessage(message, userId);
        break;
      case 'typing_start':
        broadcastToRoom(message.roomId, { type: 'typing', userId }, userId);
        break;
      case 'mark_read':
        await markMessagesRead(message.roomId, userId, message.upToTimestamp);
        break;
    }
  });
  
  ws.on('close', () => {
    clients.delete(userId);
    updatePresence(userId, 'offline');
  });
  
  // Send undelivered messages
  sendQueuedMessages(userId, ws);
});

Message Data Model

-- Conversations (direct or group)
conversations (
  id UUID PRIMARY KEY,
  type ENUM('direct', 'group'),
  name TEXT, -- null for direct
  created_at TIMESTAMPTZ
)

-- Conversation members
conversation_members (
  conversation_id UUID REFERENCES conversations(id),
  user_id UUID REFERENCES users(id),
  joined_at TIMESTAMPTZ,
  last_read_at TIMESTAMPTZ, -- for read receipts
  PRIMARY KEY (conversation_id, user_id)
)

-- Messages
messages (
  id UUID PRIMARY KEY,
  conversation_id UUID REFERENCES conversations(id),
  sender_id UUID REFERENCES users(id),
  content TEXT,
  type ENUM('text', 'image', 'file', 'system'),
  reply_to_id UUID REFERENCES messages(id),
  edited_at TIMESTAMPTZ,
  deleted_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ,
  -- Index for pagination
  INDEX (conversation_id, created_at DESC)
)

Message Delivery Guarantees

The hard part: what happens when the client disconnects mid-send?

Client-side approach:

  1. Client assigns message a local UUID before sending
  2. Client stores message locally as "pending"
  3. Server acknowledges with { ack: messageLocalId, serverMessageId, timestamp }
  4. Client marks message as "delivered" on ack

If no ack within 5 seconds → retry. Server deduplicates by sender_local_id.

// Client
function sendMessage(content) {
  const localId = crypto.randomUUID();
  pendingMessages.set(localId, content);
  
  ws.send(JSON.stringify({ 
    type: 'send_message', 
    localId, 
    content, 
    conversationId 
  }));
  
  setTimeout(() => {
    if (pendingMessages.has(localId)) retry(localId);
  }, 5000);
}

// Server acknowledges
ws.send(JSON.stringify({ type: 'ack', localId, serverId, timestamp }));

Read Receipts

Update last_read_at in conversation_members when user scrolls or opens conversation:

// Client sends when messages are visible
ws.send(JSON.stringify({
  type: 'mark_read',
  conversationId,
  upToTimestamp: lastVisibleMessage.createdAt
}));

// Server
async function markMessagesRead(conversationId, userId, timestamp) {
  await db.query(
    `UPDATE conversation_members 
     SET last_read_at = $1 
     WHERE conversation_id = $2 AND user_id = $3`,
    [timestamp, conversationId, userId]
  );
  
  // Broadcast read receipt to other members
  broadcastToRoom(conversationId, {
    type: 'read_receipt',
    userId,
    timestamp
  }, userId);
}

Presence (Online/Offline/Typing)

Use Redis for presence - it's fast and ephemeral:

// User connects
await redis.set(`presence:${userId}`, 'online', 'EX', 30); // 30s TTL

// Heartbeat every 20 seconds from client
ws.on('ping', () => redis.expire(`presence:${userId}`, 30));

// Check if user is online
const isOnline = await redis.exists(`presence:${userId}`);

// Typing indicator
await redis.set(`typing:${conversationId}:${userId}`, '1', 'EX', 5);

Scaling WebSocket Servers

Single WebSocket server handles ~10,000-50,000 concurrent connections. Beyond that:

Problem: User A is on server 1, User B is on server 2. A sends to B - server 1 doesn't know B's connection.

Solution: Redis Pub/Sub

// Server 1 (User A's connection)
await redis.publish(`user:${recipientId}`, JSON.stringify(message));

// Server 2 (User B's connection)
const sub = createClient();
await sub.subscribe(`user:${userId}`, (message) => {
  ws.send(message); // Forward to User B's WebSocket
});

Each server subscribes to channels for its connected users. Redis routes messages between servers.


Push Notifications for Offline Users

When recipient is offline, send push notification:

async function handleSendMessage(message, senderId) {
  await saveMessage(message);
  
  const recipients = await getConversationMembers(message.conversationId);
  
  for (const recipientId of recipients) {
    if (clients.has(recipientId)) {
      // Online: real-time WebSocket
      clients.get(recipientId).send(JSON.stringify(message));
    } else {
      // Offline: push notification
      await sendPushNotification(recipientId, {
        title: `New message from ${senderName}`,
        body: message.content.substring(0, 100),
        data: { conversationId: message.conversationId }
      });
    }
  }
}

Message Pagination

Never load all messages at once. Cursor-based pagination:

-- Load messages before a cursor (infinite scroll upward)
SELECT * FROM messages
WHERE conversation_id = $1
  AND created_at < $2  -- cursor
ORDER BY created_at DESC
LIMIT 50;

Load 50 messages initially. As user scrolls up, fetch older messages with the oldest message timestamp as cursor.

Build your chat application with us →


Aunimeda develops websites and web applications for businesses - corporate sites, e-commerce, portals, and custom platforms.

Contact us to discuss your web project. See also: Web Development, E-commerce Development

Read Also

How to Build an MVP in 2026: A Founder's Guide to Scope, Cost, and Speedaunimeda
Web Development

How to Build an MVP in 2026: A Founder's Guide to Scope, Cost, and Speed

What an MVP actually is (and isn't), how to scope it correctly, how much it costs in 2026, and how to choose the right development approach. Practical advice for founders and product teams.

Outsource Software Development to Kyrgyzstan: A Practical Guide for 2026aunimeda
Web Development

Outsource Software Development to Kyrgyzstan: A Practical Guide for 2026

Why businesses outsource software development to Kyrgyzstan in 2026, how to evaluate and hire a development team in Bishkek, and what the engagement process actually looks like.

SaaS Web App Development Guide 2026: Architecture, Stack, and Pricingaunimeda
Web Development

SaaS Web App Development Guide 2026: Architecture, Stack, and Pricing

How to build a SaaS web application in 2026: architecture decisions, technology stack, multi-tenancy, subscription billing, and realistic development costs. Practical guide from a development studio.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Web Development

Get Consultation All articles