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 1000+ 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 |
What Viprasol Offers
We build real-time features for web applications — from simple notification feeds through full collaborative editing systems.
→ Build real-time features →
→ Web Development Services →
Explore More
External Resources
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 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
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.