Back to Blog

Real-Time Application Development: WebSockets, SSE, and Pub/Sub Architecture

Real-time application development in 2026 — WebSockets vs Server-Sent Events vs polling, Socket.io, Redis pub/sub, and production patterns for chat, live dashbo

Viprasol Tech Team
March 30, 2026
12 min read

Real-Time Application Development: WebSockets, SSE, and Pub/Sub Architecture

Real-time features — live chat, collaborative editing, live dashboards, instant notifications — have moved from "nice to have" to baseline user expectations. Slack, Figma, Notion, and Linear have trained users to expect that changes appear instantly across all devices.

Building real-time features correctly requires understanding the transport layer, the message distribution architecture, and the scaling patterns that break if you get them wrong.

This guide covers all three: transport selection, implementation patterns, and production architecture that scales horizontally.


Transport Layer: WebSocket vs SSE vs Polling

WebSocket

Full-duplex, persistent connection. Both client and server can send messages at any time.

Use WebSocket when:

  • Bidirectional communication (client sends messages, server responds)
  • High message frequency (>1 message/second)
  • Chat, collaborative editing, multiplayer, live trading feeds

WebSocket limitations:

  • Doesn't work through some proxies/load balancers without configuration
  • Stateful connection (breaks horizontal scaling without sticky sessions or pub/sub)
  • More complex to implement than SSE

Server-Sent Events (SSE)

Unidirectional: server pushes events to client over a persistent HTTP connection.

Use SSE when:

  • Server → client only (notifications, live feeds, progress updates)
  • Works through standard HTTP proxies without configuration
  • Simple implementation needed

SSE limitations:

  • Browser limit of 6 concurrent SSE connections per origin (HTTP/1.1) — HTTP/2 removes this
  • Client can't send messages (must use separate HTTP requests)

Long Polling

HTTP request held open until data is available, then response sent and client re-connects.

Use long polling when:

  • SSE/WebSocket are blocked by network infrastructure
  • Update frequency is low (<1/minute)
  • Simplest possible implementation is required

Comparison Table

FeatureWebSocketSSELong Polling
DirectionBidirectionalServer → ClientServer → Client
Protocolws:// / wss://HTTPHTTP
Proxy-friendly❌ (needs config)
Message frequencyHighMediumLow
Browser support97%+96%+100%
Implementation complexityHighLowLow
Horizontal scalingComplexComplexEasy

WebSocket Implementation (Socket.io + Node.js)

Socket.io adds rooms, namespaces, automatic reconnection, and fallback to polling — essential for production use.

// server.ts
import { createServer } from 'http';
import { Server, Socket } from 'socket.io';
import express from 'express';

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.CLIENT_ORIGIN,
    methods: ['GET', 'POST'],
    credentials: true,
  },
  // Adapter for horizontal scaling (see below)
  adapter: createAdapter(redisClient, redisSubClient),
});

// Authentication middleware
io.use(async (socket: Socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    const user = verifyAccessToken(token);
    socket.data.user = user;
    next();
  } catch {
    next(new Error('Authentication failed'));
  }
});

// Connection handler
io.on('connection', (socket: Socket) => {
  const user = socket.data.user;
  console.log(`User ${user.sub} connected`);

  // Join workspace room
  socket.on('join_workspace', async (workspaceId: string) => {
    const hasAccess = await verifyWorkspaceAccess(user.sub, workspaceId);
    if (!hasAccess) {
      socket.emit('error', { message: 'Access denied' });
      return;
    }
    socket.join(`workspace:${workspaceId}`);
    socket.emit('joined', { workspaceId });
  });

  // Chat message
  socket.on('message', async (data: { workspaceId: string; content: string }) => {
    const message = await db('messages').insert({
      workspace_id: data.workspaceId,
      user_id: user.sub,
      content: data.content.slice(0, 5000), // Enforce limit
      created_at: new Date(),
    }).returning('*');

    // Broadcast to all members of the workspace room
    io.to(`workspace:${data.workspaceId}`).emit('message', {
      id: message[0].id,
      userId: user.sub,
      content: message[0].content,
      createdAt: message[0].created_at,
    });
  });

  socket.on('disconnect', () => {
    console.log(`User ${user.sub} disconnected`);
  });
});
// client.ts (React)
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';

interface Message {
  id: string;
  userId: string;
  content: string;
  createdAt: string;
}

function useWorkspaceChat(workspaceId: string) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [connected, setConnected] = useState(false);
  const socketRef = useRef<Socket | null>(null);

  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_WS_URL!, {
      auth: { token: getAccessToken() },
      reconnectionAttempts: 5,
      reconnectionDelay: 1000,
    });

    socket.on('connect', () => {
      setConnected(true);
      socket.emit('join_workspace', workspaceId);
    });

    socket.on('disconnect', () => setConnected(false));

    socket.on('message', (msg: Message) => {
      setMessages(prev => [...prev, msg]);
    });

    socketRef.current = socket;
    return () => { socket.disconnect(); };
  }, [workspaceId]);

  const sendMessage = (content: string) => {
    socketRef.current?.emit('message', { workspaceId, content });
  };

  return { messages, connected, sendMessage };
}

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Server-Sent Events (SSE)

// SSE endpoint — live notifications, progress updates
app.get('/api/notifications/stream', authenticate, (req, res) => {
  // SSE headers
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no',  // Disable nginx buffering
  });

  res.write('data: {"type":"connected"}\n\n');

  const userId = req.user.sub;
  const channel = `notifications:${userId}`;

  // Subscribe to Redis channel for this user
  const subscriber = redis.duplicate();
  subscriber.subscribe(channel, (err) => {
    if (err) return res.end();
  });

  subscriber.on('message', (_channel: string, message: string) => {
    res.write(`data: ${message}\n\n`);
  });

  // Keep-alive ping every 30 seconds
  const keepAlive = setInterval(() => {
    res.write(':ping\n\n');
  }, 30000);

  // Cleanup on client disconnect
  req.on('close', () => {
    clearInterval(keepAlive);
    subscriber.unsubscribe();
    subscriber.quit();
  });
});
// Send notification from anywhere in the application
async function sendNotification(userId: string, notification: Notification): Promise<void> {
  // Save to database
  await db('notifications').insert({
    user_id: userId,
    type: notification.type,
    data: JSON.stringify(notification.data),
    created_at: new Date(),
  });

  // Publish to Redis channel (SSE subscribers pick this up)
  await redis.publish(`notifications:${userId}`, JSON.stringify(notification));
}

Horizontal Scaling: Redis Pub/Sub Adapter

The critical scaling challenge: when you have 3 WebSocket servers, a message to user A (connected to server 1) from user B (connected to server 2) doesn't reach its destination — unless you use a pub/sub broker.

// Socket.io with Redis adapter — messages fan out across all servers
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

const io = new Server(httpServer, {
  adapter: createAdapter(pubClient, subClient),
});

// Now io.to('workspace:123').emit(...) reaches ALL clients in that room
// regardless of which server they're connected to
Server 1 (3 users)  ←→  Redis Pub/Sub  ←→  Server 2 (4 users)
                                 ↕
                         Server 3 (2 users)

When Server 1 emits to room "workspace:abc":
→ Redis broadcasts to all servers
→ Each server delivers to its locally-connected room members
→ All 9 users receive the message

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Collaborative Editing: Operational Transform vs CRDT

For real-time document collaboration (Notion, Figma-style):

ApproachHow It WorksLibrariesUse When
Operational Transform (OT)Server transforms conflicting operationsShareDB, OT.jsSimple text, established approach
CRDT (Conflict-free Replicated Data Types)Data structures that always convergeYjs, AutomergeRich text, offline support, complex structures
// Yjs — CRDT-based collaborative text editing
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { QuillBinding } from 'y-quill';

// On the client
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('wss://ws.example.com', `doc-${docId}`, ydoc);
const ytext = ydoc.getText('content');

// Bind to editor — conflict resolution handled automatically by CRDT
const binding = new QuillBinding(ytext, quillEditor, provider.awareness);

// On the server (y-websocket server handles sync)
// No custom conflict resolution needed — CRDTs converge automatically

Presence Indicators (Online/Typing)

// Track user presence using Redis with expiry
async function setUserPresent(userId: string, workspaceId: string): Promise<void> {
  await redis.setex(`presence:${workspaceId}:${userId}`, 30, '1'); // 30-second TTL
}

async function getWorkspacePresence(workspaceId: string): Promise<string[]> {
  const keys = await redis.keys(`presence:${workspaceId}:*`);
  return keys.map(key => key.split(':')[2]); // Extract user IDs
}

// Socket.io typing indicator
socket.on('typing_start', ({ workspaceId }: { workspaceId: string }) => {
  socket.to(`workspace:${workspaceId}`).emit('user_typing', { userId: user.sub });
});

socket.on('typing_stop', ({ workspaceId }: { workspaceId: string }) => {
  socket.to(`workspace:${workspaceId}`).emit('user_stopped_typing', { userId: user.sub });
});

Infrastructure Costs

ScaleArchitectureMonthly Cost
< 1,000 concurrent1 Node.js server + Redis$50–$100
1,000–10,000 concurrent3 Node.js servers + Redis$200–$500
10,000–100,000 concurrent10+ servers + Redis Cluster$1,000–$3,000
100,000+ concurrentDedicated WS infrastructure (Ably, Pusher)$500–$5,000+/month

Managed WebSocket services (2026): Ably ($29+/month), Pusher ($49+/month), Supabase Realtime (free tier), AWS API Gateway WebSocket ($1/million messages). For most applications under 50k concurrent users, self-hosted Socket.io + Redis is more cost-effective.


Build Cost

ScopeInvestment
Basic notifications (SSE)$5,000–$12,000
Chat feature (WebSocket)$12,000–$25,000
Live dashboard$8,000–$20,000
Collaborative editing (Yjs/CRDT)$20,000–$50,000
Full real-time platform$40,000–$100,000

Working With Viprasol

We build real-time features for web applications — from simple notification feeds through full collaborative editing systems.

Build real-time features →
Web Development Services →


See Also


Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.