Real-time Architecture in 2026: WebSockets, SSE and WebRTC, when to use each
Data streaming, push notifications and peer-to-peer communication demand correct architectural choices.
Executive summary
Data streaming, push notifications and peer-to-peer communication demand correct architectural choices.
Last updated: 3/12/2026
Introduction: The end of the polling era
The traditional polling architecture — the client asking the server "is there anything new?" every X seconds — is dead. It's inefficient, wastes bandwidth, delivers inconsistent latency, and scales poorly. In 2026, any modern application requiring real-time updates needs one of three fundamental technologies: WebSockets, Server-Sent Events (SSE), or WebRTC.
Each serves a distinct purpose. Choosing the wrong one means wasting resources, creating scalability problems, or implementing features that simply won't work in production.
The spectrum of real-time needs
Before choosing the technology, you need to understand what you actually need:
Latency ══════════════════════════════════════════►
Low Medium High
Duplex ══════════════════════════════════════════►
direction Unidirectional Bidirectional
Scale ══════════════════════════════════════════►
1-1 1-N N-N
Connection ══════════════════════════════════════════►
Client-Server P2PWebSockets: bidirectional full-duplex communication
WebSockets are the standard for persistent bidirectional communication between client and server. Both parties can send messages at any time without HTTP overhead.
WebSocket architecture
┌──────────────┐ HTTP Upgrade ┌──────────────┐
│ Browser │ ◄══════════════════════════► │ Server │
│ │ │ │
│ Client │ ◄──── JSON Message ───────► │ Handler │
│ │ │ │
│ │ ───── Acknowledgment ────► │ │
└──────────────┘ └──────────────┘
│ │
│ Event Handler Event Emitter
└─────────────────────────────────────────────────┘When to use WebSockets
Collaborative applications:
- Real-time document editors (Google Docs style)
- Collaborative whiteboards
- Team project management tools
Bidirectional data streaming:
- Real-time chats
- Multiplayer games
- Remote device control
Monitoring and dashboards:
- Operations dashboards with continuous updates
- Real-time system monitoring
- Notification feeds with confirmations
Implementation with Socket.IO
typescript// Server: Node.js with Socket.IO
import { Server } from 'socket.io';
import { createServer } from 'http';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true
},
transports: ['websocket', 'polling'], // Fallback to polling
pingTimeout: 60000,
pingInterval: 25000
});
// Authentication middleware
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication error'));
}
try {
const user = await verifyToken(token);
socket.data.user = user;
next();
} catch (error) {
next(new Error('Authentication error'));
}
});
// Room management
io.on('connection', (socket) => {
const user = socket.data.user;
console.log(`User connected: ${user.id}`);
// User joins their organization room
socket.join(`org:${user.organizationId}`);
// Personal room for direct notifications
socket.join(`user:${user.id}`);
// Handle incoming messages
socket.on('message:send', async (data) => {
const { roomId, content } = data;
// Save message to database
const message = await saveMessage({
userId: user.id,
roomId,
content,
timestamp: new Date()
});
// Broadcast to everyone in room (except sender)
socket.to(roomId).emit('message:received', message);
// Acknowledge to sender
socket.emit('message:ack', { id: message.id });
});
// Handle presence (typing...)
socket.on('typing:start', (data) => {
const { roomId } = data;
socket.to(roomId).emit('typing:started', {
userId: user.id,
userName: user.name
});
});
socket.on('typing:stop', (data) => {
const { roomId } = data;
socket.to(roomId).emit('typing:stopped', {
userId: user.id
});
});
// Graceful disconnect
socket.on('disconnect', () => {
console.log(`User disconnected: ${user.id}`);
// Notify departure
socket.to(`org:${user.organizationId}`).emit('user:left', {
userId: user.id,
userName: user.name
});
});
});
// Broadcast to entire organization
export function broadcastToOrganization(orgId: string, event: string, data: any) {
io.to(`org:${orgId}`).emit(event, data);
}
// Send to specific user
export function sendToUser(userId: string, event: string, data: any) {
io.to(`user:${userId}`).emit(event, data);
}
httpServer.listen(3001);TypeScript/React client
typescript// Client: React with Socket.IO Client
import { useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';
import { useAuth } from '@/hooks/useAuth';
export function RealtimeChat({ roomId }: { roomId: string }) {
const socketRef = useRef<Socket | null>(null);
const { token } = useAuth();
useEffect(() => {
// Connect to WebSocket server
const socket = io(process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001', {
auth: { token },
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
});
socketRef.current = socket;
// Join room
socket.emit('join:room', { roomId });
// Listen for messages
socket.on('message:received', (message) => {
// Update state
appendMessage(message);
});
// Listen for typing status
socket.on('typing:started', (data) => {
showTypingIndicator(data);
});
socket.on('typing:stopped', (data) => {
hideTypingIndicator(data);
});
// Cleanup
return () => {
socket.disconnect();
};
}, [roomId, token]);
const sendMessage = (content: string) => {
if (socketRef.current) {
socketRef.current.emit('message:send', { roomId, content });
}
};
return (
<div className="chat-container">
{/* Chat UI */}
</div>
);
}Production considerations
Connection management:
typescript// Rate limiting connections per IP
const connectionTracker = new Map<string, number>();
io.use((socket, next) => {
const ip = socket.handshake.address;
const count = connectionTracker.get(ip) || 0;
if (count >= 10) {
return next(new Error('Too many connections'));
}
connectionTracker.set(ip, count + 1);
socket.on('disconnect', () => {
const current = connectionTracker.get(ip) || 0;
connectionTracker.set(ip, Math.max(0, current - 1));
});
next();
});Horizontal scaling with Redis:
typescript// For multiple WebSocket servers, use Redis Adapter
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()
]);
io.adapter(createAdapter(pubClient, subClient));Server-Sent Events (SSE): simple unidirectional streaming
SSE is a much simpler technology than WebSockets: the server sends events to the client in a continuous stream, but the client cannot send messages back over the same channel.
SSE architecture
┌──────────────┐ HTTP GET /stream ┌──────────────┐
│ Browser │ ◄════════════════════════► │ Server │
│ │ │ │
│ EventSource│ ◄────── data: {} ──────────► │ Generator │
│ │ │ │
│ │ ◄────── retry: 3000 ──────► │ │
└──────────────┘ └──────────────┘When to use SSE
Data streaming:
- Real-time stock price feeds
- IoT updates (sensors sending data)
- Log streaming for dashboards
Push notifications:
- Social feed updates
- System alerts
- App notifications
SSE implementation with Node.js
typescript// SSE Server with Express
import express from 'express';
import { Server } from 'http';
import { verifyToken } from './auth';
const app = express();
const server = new Server(app);
interface SSEConnection {
userId: string;
response: express.Response;
lastHeartbeat: number;
}
const connections = new Map<string, SSEConnection>();
app.get('/stream', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).end();
}
try {
const user = await verifyToken(token);
// Configure SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // For nginx
// Store connection
const connection: SSEConnection = {
userId: user.id,
response: res,
lastHeartbeat: Date.now()
};
connections.set(user.id, connection);
// Send initial event
sendEvent(res, 'connected', { userId: user.id, timestamp: Date.now() });
// Heartbeat
const heartbeatInterval = setInterval(() => {
sendEvent(res, 'heartbeat', { timestamp: Date.now() });
}, 30000);
// Cleanup when client disconnects
req.on('close', () => {
clearInterval(heartbeatInterval);
connections.delete(user.id);
});
// Cleanup on timeout
const timeout = setTimeout(() => {
clearInterval(heartbeatInterval);
connections.delete(user.id);
res.end();
}, 5 * 60 * 1000); // 5 minutes max
} catch (error) {
res.status(401).end();
}
});
// Send event to specific user
export function sendSSEEvent(userId: string, type: string, data: any) {
const connection = connections.get(userId);
if (connection) {
sendEvent(connection.response, type, data);
}
}
function sendEvent(res: express.Response, type: string, data: any) {
const event = `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
res.write(event);
}
// Broadcast to all
export function broadcastSSEEvent(type: string, data: any) {
for (const [userId, connection] of connections) {
sendEvent(connection.response, type, data);
}
}Client with EventSource
typescript// Client: Native EventSource API
class SSEClient {
private eventSource: EventSource | null = null;
connect(url: string, token: string) {
this.eventSource = new EventSource(`${url}?token=${token}`);
this.eventSource.addEventListener('connected', (event) => {
console.log('SSE Connected:', JSON.parse(event.data));
});
this.eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
this.handleNotification(data);
});
this.eventSource.addEventListener('heartbeat', () => {
// Keep connection alive
});
this.eventSource.onerror = (error) => {
console.error('SSE Error:', error);
// EventSource automatically reconnects
};
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
private handleNotification(data: any) {
// Process notification
}
}Advantages and limitations
Advantages:
- Extremely simple implementation
- Native in browsers (EventSource API)
- Automatically reconnects
- Works with standard HTTP proxies
- Uses only one HTTP connection
Limitations:
- Unidirectional only (server → client)
- No binary data support (text only)
- Limited to UTF-8
- Each request creates new connection (doesn't multiplex)
WebRTC: direct peer-to-peer communication
WebRTC enables direct communication between browsers without passing through the server. It's the technology behind video conferencing, P2P file sharing, and multiplayer games.
WebRTC architecture
┌──────────────┐ ┌──────────────┐
│ Browser A │ │ Browser B │
│ │ │ │
│ Peer │ ←──── P2P Stream ────► │ Peer │
│ │ │ │
└──────────────┘ └──────────────┘
▲ ▲
│ │
│ ┌──────────────┐ │
└─────────────│ Signaling │──────────────┘
│ Server │
│ (WebSocket)│
└──────────────┘When to use WebRTC
Low latency communication:
- Video conferencing
- Voice over IP
- Real-time multiplayer games
Large data transfer:
- P2P file sharing
- Video streaming
- Large data synchronization
WebRTC implementation with SimplePeer
typescript// Client: WebRTC with SimplePeer
import SimplePeer from 'simple-peer';
import { socket } from './websocket-client';
class WebRTCClient {
private peers: Map<string, SimplePeer.Instance> = new Map();
private localStream: MediaStream | null = null;
async initialize(userId: string) {
// Get local media stream (camera/mic)
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// Listen for signals from signaling server
socket.on('webrtc:signal', ({ from, signal }) => {
this.handleSignal(from, signal);
});
socket.on('webrtc:user_joined', ({ userId }) => {
this.createPeer(userId);
});
socket.on('webrtc:user_left', ({ userId }) => {
this.destroyPeer(userId);
});
// Announce join
socket.emit('webrtc:join', { userId });
}
private createPeer(userId: string) {
const peer = new SimplePeer({
initiator: true,
trickle: false,
stream: this.localStream!,
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:global.stun.twilio.com:3478' }
]
}
});
peer.on('signal', (signal) => {
// Send signal to other peer via signaling server
socket.emit('webrtc:signal', {
to: userId,
signal
});
});
peer.on('stream', (stream) => {
// Receive remote peer stream
this.handleRemoteStream(userId, stream);
});
peer.on('connect', () => {
console.log(`Connected to peer ${userId}`);
});
peer.on('close', () => {
console.log(`Connection closed with peer ${userId}`);
this.peers.delete(userId);
});
this.peers.set(userId, peer);
}
private handleSignal(from: string, signal: any) {
let peer = this.peers.get(from);
if (!peer) {
// If first signal, create peer (non-initiator)
peer = new SimplePeer({
initiator: false,
trickle: false,
stream: this.localStream!
});
peer.on('signal', (signal) => {
socket.emit('webrtc:signal', {
to: from,
signal
});
});
peer.on('stream', (stream) => {
this.handleRemoteStream(from, stream);
});
peer.on('close', () => {
this.peers.delete(from);
});
this.peers.set(from, peer);
}
// Process signal
peer.signal(signal);
}
private handleRemoteStream(userId: string, stream: MediaStream) {
// Display remote stream (e.g., in a video element)
const videoElement = document.getElementById(`video-${userId}`) as HTMLVideoElement;
if (videoElement) {
videoElement.srcObject = stream;
}
}
private destroyPeer(userId: string) {
const peer = this.peers.get(userId);
if (peer) {
peer.destroy();
this.peers.delete(userId);
}
}
cleanup() {
for (const peer of this.peers.values()) {
peer.destroy();
}
this.peers.clear();
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
}
}
}Signaling server
typescript// Server: WebSocket for WebRTC signaling
io.on('connection', (socket) => {
socket.on('webrtc:join', ({ userId }) => {
socket.data.userId = userId;
socket.join('video-call');
// Notify other users
socket.to('video-call').emit('webrtc:user_joined', { userId });
});
socket.on('webrtc:signal', ({ to, signal }) => {
// Forward signal to destination peer
socket.to(to).emit('webrtc:signal', {
from: socket.data.userId,
signal
});
});
socket.on('disconnect', () => {
const userId = socket.data.userId;
if (userId) {
socket.to('video-call').emit('webrtc:user_left', { userId });
}
});
});Final comparison
| Feature | WebSocket | SSE | WebRTC |
|---|---|---|---|
| Direction | Bidirectional | Server → Client | Bidirectional |
| Latency | Low | Low | Very low |
| Simplicity | Medium | High | Low |
| Scalability | Medium* | High* | N/A (P2P) |
| Binary data | Yes | No | Yes |
| Proxy friendly | Yes | Yes | Difficult |
| Mobile battery | Medium impact | Low impact | High impact |
| Primary use | Chat, collaboration | Notifications, streaming | Video, P2P |
\With Redis adapter or similar*
Your application needs real-time communication, but you're not sure which technology to choose? Talk to Imperialis specialists about real-time communication architectures, from WebSockets to WebRTC, to scale with performance and reliability.
Sources
- MDN WebSockets API — WebSockets documentation
- MDN Server-Sent Events — SSE documentation
- MDN WebRTC API — WebRTC documentation
- Socket.IO documentation — Socket.IO guide