Cloud and platform

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.

3/12/20269 min readCloud
Real-time Architecture in 2026: WebSockets, SSE and WebRTC, when to use each

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           P2P

WebSockets: 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

FeatureWebSocketSSEWebRTC
DirectionBidirectionalServer → ClientBidirectional
LatencyLowLowVery low
SimplicityMediumHighLow
ScalabilityMedium*High*N/A (P2P)
Binary dataYesNoYes
Proxy friendlyYesYesDifficult
Mobile batteryMedium impactLow impactHigh impact
Primary useChat, collaborationNotifications, streamingVideo, 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

Related reading