← All articlesPlatform Engineering

WebSocket Alternatives: SSE, WebTransport, and MQTT Compared

WebSockets are not the only option for real-time communication. Compare Server-Sent Events, WebTransport, and MQTT for different use cases from dashboards...

Y
Yash Pritwani
13 min read

Beyond WebSockets

WebSockets have been the default choice for real-time web communication since 2011. But in 2025, we have better options for many use cases. The key insight is that most "real-time" features are actually server-to-client push, not bidirectional communication.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="220" rx="12" fill="#1a1a2e"/><rect x="230" y="15" width="140" height="35" rx="8" fill="#6366f1" opacity="0.9"/><text x="300" y="38" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">API Gateway</text><rect x="30" y="80" width="100" height="50" rx="8" fill="#3b82f6" opacity="0.8"/><text x="80" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Auth</text><text x="80" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service</text><rect x="160" y="80" width="100" height="50" rx="8" fill="#a855f7" opacity="0.8"/><text x="210" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">User</text><text x="210" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service</text><rect x="290" y="80" width="100" height="50" rx="8" fill="#2dd4bf" opacity="0.8"/><text x="340" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Order</text><text x="340" y="115" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service</text><rect x="420" y="80" width="100" height="50" rx="8" fill="#f59e0b" opacity="0.8"/><text x="470" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Payment</text><text x="470" y="115" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service</text><line x1="265" y1="50" x2="80" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="285" y1="50" x2="210" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="315" y1="50" x2="340" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="335" y1="50" x2="470" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><ellipse cx="80" cy="175" rx="35" ry="12" fill="none" stroke="#3b82f6" stroke-width="1.5"/><line x1="45" y1="175" x2="45" y2="190" stroke="#3b82f6" stroke-width="1.5"/><line x1="115" y1="175" x2="115" y2="190" stroke="#3b82f6" stroke-width="1.5"/><ellipse cx="80" cy="190" rx="35" ry="12" fill="none" stroke="#3b82f6" stroke-width="1.5"/><line x1="80" y1="130" x2="80" y2="163" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><ellipse cx="340" cy="175" rx="35" ry="12" fill="none" stroke="#2dd4bf" stroke-width="1.5"/><line x1="305" y1="175" x2="305" y2="190" stroke="#2dd4bf" stroke-width="1.5"/><line x1="375" y1="175" x2="375" y2="190" stroke="#2dd4bf" stroke-width="1.5"/><ellipse cx="340" cy="190" rx="35" ry="12" fill="none" stroke="#2dd4bf" stroke-width="1.5"/><line x1="340" y1="130" x2="340" y2="163" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><rect x="155" y="160" width="150" height="30" rx="6" fill="#a855f7" opacity="0.3"/><text x="230" y="180" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">Message Bus / Events</text><line x1="210" y1="130" x2="210" y2="158" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><line x1="470" y1="130" x2="470" y2="175" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><line x1="305" y1="175" x2="470" y2="175" stroke="#94a3b8" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Microservices architecture: independent services communicate through an API gateway and event bus.</p></div>

Server-Sent Events (SSE): The Underrated Champion

SSE is an HTTP-based protocol for server-to-client streaming. It uses a regular HTTP connection, supports automatic reconnection, and works through proxies and firewalls without any special configuration.

// Node.js SSE server
import express from 'express';
const app = express();

app.get('/api/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  // Send heartbeat every 30s to keep connection alive
  const heartbeat = setInterval(() => {
    res.write(':heartbeat\n\n');
  }, 30000);

  // Send events
  const sendEvent = (event, data: object) => {
    res.write('event: ' + event + '\n');
    res.write('data: ' + JSON.stringify(data) + '\n\n');
  };

  // Example: stream deployment progress
  sendEvent('deploy-start', { service: 'api', version: 'v2.1' });
  sendEvent('deploy-progress', { step: 'building', percent: 30 });
  sendEvent('deploy-progress', { step: 'pushing', percent: 60 });
  sendEvent('deploy-complete', { status: 'success', url: 'https://api.example.com' });

  req.on('close', () => {
    clearInterval(heartbeat);
  });
});
// Browser client
const events = new EventSource('/api/events');

events.addEventListener('deploy-progress', (e) => {
  const data = JSON.parse(e.data);
  updateProgressBar(data.percent);
});

events.addEventListener('deploy-complete', (e) => {
  const data = JSON.parse(e.data);
  showNotification('Deployment complete: ' + data.status);
});

// Automatic reconnection is built-in
events.onerror = () => {
  console.log('Connection lost, reconnecting automatically...');
};

SSE advantages over WebSockets:

Uses standard HTTP (works with all proxies, CDNs, and load balancers)
Automatic reconnection with last-event-ID
Simpler server implementation (no upgrade handshake)
Works with HTTP/2 multiplexing
EventSource API is simpler than WebSocket API

SSE limitations:

Server-to-client only (no client-to-server messages)
Text-based only (no binary data)
Limited to ~6 connections per domain in HTTP/1.1 (not an issue with HTTP/2)

WebTransport: The Next Generation

WebTransport is a new protocol built on HTTP/3 (QUIC) that provides reliable and unreliable, ordered and unordered, bidirectional communication. It is the future replacement for WebSockets.

// WebTransport client
const transport = new WebTransport('https://example.com:4433/game');

await transport.ready;

// Reliable bidirectional stream (like WebSocket)
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();

await writer.write(new TextEncoder().encode('player-move:x=10,y=20'));

const { value } = await reader.read();
console.log(new TextDecoder().decode(value));

// Unreliable datagrams (like UDP, for real-time games)
const dgWriter = transport.datagrams.writable.getWriter();
await dgWriter.write(new Uint8Array([0x01, 0x02, 0x03]));

// Read incoming datagrams
const dgReader = transport.datagrams.readable.getReader();
while (true) {
  const { value, done } = await dgReader.read();
  if (done) break;
  handleGameState(value);
}

WebTransport advantages:

Multiplexed streams (multiple independent streams, no head-of-line blocking)
Unreliable datagrams (perfect for games, video, real-time sensor data)
Built on QUIC (faster connection establishment, better mobile performance)
No head-of-line blocking (unlike WebSocket over TCP)

WebTransport limitations:

Requires HTTP/3 server support
Browser support is still growing (Chrome and Edge, Firefox partial)
Server libraries are less mature
More complex API than WebSocket or SSE

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 180" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="180" rx="12" fill="#1a1a2e"/><rect x="20" y="20" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="42" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Web</text><rect x="20" y="65" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="87" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Mobile</text><rect x="20" y="110" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="132" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">IoT</text><rect x="150" y="20" width="120" height="130" rx="10" fill="#6366f1" opacity="0.9"/><text x="210" y="50" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">Gateway</text><line x1="165" y1="60" x2="255" y2="60" stroke="#ffffff" stroke-width="0.5" opacity="0.3"/><text x="210" y="80" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Rate Limit</text><text x="210" y="95" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Auth</text><text x="210" y="110" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Load Balance</text><text x="210" y="125" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Transform</text><text x="210" y="140" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Cache</text><rect x="340" y="15" width="95" height="35" rx="6" fill="#a855f7" opacity="0.8"/><text x="387" y="37" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service A</text><rect x="340" y="60" width="95" height="35" rx="6" fill="#2dd4bf" opacity="0.8"/><text x="387" y="82" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service B</text><rect x="340" y="105" width="95" height="35" rx="6" fill="#f59e0b" opacity="0.8"/><text x="387" y="127" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service C</text><rect x="490" y="55" width="80" height="45" rx="6" fill="none" stroke="#e2e8f0" stroke-width="1"/><text x="530" y="82" text-anchor="middle" fill="#e2e8f0" font-size="10" font-family="system-ui">DB / Cache</text><defs><marker id="arrow7" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="92" y1="37" x2="148" y2="55" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="92" y1="82" x2="148" y2="85" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="92" y1="127" x2="148" y2="115" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="55" x2="338" y2="32" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="85" x2="338" y2="77" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="115" x2="338" y2="122" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="437" y1="77" x2="488" y2="77" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">API gateway pattern: a single entry point handles auth, rate limiting, and routing to backend services.</p></div>

MQTT: The IoT Standard

MQTT (Message Queuing Telemetry Transport) is a lightweight publish/subscribe protocol designed for constrained networks and IoT devices.

# Python MQTT publisher
import paho.mqtt.client as mqtt
import json

client = mqtt.Client(client_id="sensor-001", protocol=mqtt.MQTTv5)
client.tls_set()
client.username_pw_set("device", "secret")
client.connect("mqtt.techsaas.cloud", 8883)

# Publish sensor data
while True:
    payload = json.dumps({
        "temperature": read_sensor(),
        "humidity": read_humidity(),
        "timestamp": time.time(),
    })
    client.publish(
        "sensors/building-a/floor-2/room-201",
        payload,
        qos=1,  # At least once delivery
        retain=True,  # New subscribers get last value
    )
    time.sleep(10)
// JavaScript MQTT subscriber (Node.js or browser via WebSocket)
import mqtt from 'mqtt';

const client = mqtt.connect('wss://mqtt.techsaas.cloud:8884');

client.on('connect', () => {
  // Subscribe with wildcard
  client.subscribe('sensors/building-a/#', { qos: 1 });
});

client.on('message', (topic, message) => {
  const data = JSON.parse(message.toString());
  console.log('Topic: ' + topic + ', Temp: ' + data.temperature);
});

MQTT advantages:

Tiny overhead (2-byte minimum header)
Quality of Service levels (0: fire-and-forget, 1: at-least-once, 2: exactly-once)
Retained messages (new subscribers get the last value immediately)
Last Will and Testament (detect disconnected devices)
Wildcard subscriptions (sensors/+/temperature, sensors/#)

Comparison Table

Feature
WebSocket
SSE
WebTransport
MQTT

|---------|-----------|-----|--------------|------|

Direction
Bidirectional
Server→Client
Bidirectional
Pub/Sub
Transport
TCP
HTTP
QUIC (UDP)
TCP/WebSocket
Binary data
Yes
No
Yes
Yes
Auto-reconnect
Manual
Built-in
Manual
Library-level
Multiplexing
No
HTTP/2
Yes (native)
N/A
Unreliable delivery
No
No
Yes (datagrams)
QoS 0
Browser support
Universal
Universal
Chrome, Edge
Via WebSocket
Proxy-friendly
Needs upgrade
Yes (standard HTTP)
Needs HTTP/3
Needs broker
Overhead per message
2-14 bytes
~20 bytes
1-3 bytes
2+ bytes
Best for
Chat, collaboration
Dashboards, feeds
Games, video
IoT, sensors

Decision Framework

Is communication server-to-client only?
├── Yes → SSE (simplest, most reliable)
└── No → Is latency critical (sub-10ms)?
    ├── Yes → Is unreliable delivery OK?
    │   ├── Yes → WebTransport datagrams
    │   └── No → WebTransport streams or WebSocket
    └── No → Is it IoT / many devices?
        ├── Yes → MQTT
        └── No → WebSocket (most compatible)

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 170" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="170" rx="12" fill="#1a1a2e"/><circle cx="60" cy="85" r="25" fill="#f59e0b" opacity="0.85"/><text x="60" y="82" text-anchor="middle" fill="#1a1a2e" font-size="9" font-family="system-ui" font-weight="bold">Trigger</text><text x="60" y="94" text-anchor="middle" fill="#1a1a2e" font-size="8" font-family="system-ui">webhook</text><polygon points="175,55 210,85 175,115 140,85" fill="#6366f1" opacity="0.85"/><text x="175" y="88" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">If</text><rect x="250" y="35" width="100" height="40" rx="6" fill="#2dd4bf" opacity="0.85"/><text x="300" y="55" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Send Email</text><text x="300" y="67" text-anchor="middle" fill="#1a1a2e" font-size="8" font-family="system-ui">SMTP</text><rect x="250" y="95" width="100" height="40" rx="6" fill="#a855f7" opacity="0.85"/><text x="300" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Log Event</text><text x="300" y="127" text-anchor="middle" fill="#ffffff" font-size="8" font-family="system-ui">database</text><rect x="400" y="55" width="100" height="40" rx="6" fill="#3b82f6" opacity="0.85"/><text x="450" y="75" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Update CRM</text><text x="450" y="87" text-anchor="middle" fill="#ffffff" font-size="8" font-family="system-ui">API call</text><circle cx="545" cy="75" r="18" fill="none" stroke="#2dd4bf" stroke-width="2"/><text x="545" y="79" text-anchor="middle" fill="#2dd4bf" font-size="9" font-family="system-ui">Done</text><defs><marker id="arrow10" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="87" y1="85" x2="138" y2="85" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="210" y1="72" x2="248" y2="55" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="210" y1="98" x2="248" y2="115" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="352" y1="55" x2="398" y2="68" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="352" y1="115" x2="398" y2="82" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="502" y1="75" x2="525" y2="75" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><text x="225" y="45" text-anchor="middle" fill="#2dd4bf" font-size="8" font-family="system-ui">true</text><text x="225" y="120" text-anchor="middle" fill="#a855f7" font-size="8" font-family="system-ui">false</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Workflow automation: triggers, conditions, and actions chain together to eliminate manual processes.</p></div>

Real-World Architecture Example

At TechSaaS, our monitoring stack uses SSE for the dashboard:

Containers → Promtail → Loki → Grafana SSE → Browser Dashboard
Uptime Kuma → SSE stream → Status Page
CI/CD Runner → SSE events → Deployment Progress UI

For most web applications, SSE handles 80% of real-time needs without the complexity of WebSockets. Use WebSockets when you truly need bidirectional communication (collaborative editing, chat). Save WebTransport for when you need unreliable datagrams (games, live video). Use MQTT when you have constrained devices publishing sensor data.

#websocket#sse#webtransport#mqtt#real-time

Need help with platform engineering?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.