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
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
| Feature | WebSocket | SSE | Long Polling |
|---|---|---|---|
| Direction | Bidirectional | Server → Client | Server → Client |
| Protocol | ws:// / wss:// | HTTP | HTTP |
| Proxy-friendly | ❌ (needs config) | ✅ | ✅ |
| Message frequency | High | Medium | Low |
| Browser support | 97%+ | 96%+ | 100% |
| Implementation complexity | High | Low | Low |
| Horizontal scaling | Complex | Complex | Easy |
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):
| Approach | How It Works | Libraries | Use When |
|---|---|---|---|
| Operational Transform (OT) | Server transforms conflicting operations | ShareDB, OT.js | Simple text, established approach |
| CRDT (Conflict-free Replicated Data Types) | Data structures that always converge | Yjs, Automerge | Rich 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
| Scale | Architecture | Monthly Cost |
|---|---|---|
| < 1,000 concurrent | 1 Node.js server + Redis | $50–$100 |
| 1,000–10,000 concurrent | 3 Node.js servers + Redis | $200–$500 |
| 10,000–100,000 concurrent | 10+ servers + Redis Cluster | $1,000–$3,000 |
| 100,000+ concurrent | Dedicated 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
| Scope | Investment |
|---|---|
| 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
About the Author
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.
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
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.