WebSocket for Real-Time IoT Dashboards — Architecture, Implementation, and Scaling
How to use WebSocket for building real-time IoT dashboards — from protocol fundamentals to practical implementation with Node.js, MQTT bridge, and frontend visualization.
Why WebSocket for IoT?
Traditional HTTP follows a request-response pattern — the client asks, the server answers. For IoT dashboards displaying live sensor data, this model is inefficient:
| Approach | Mechanism | Latency | Overhead | |----------|-----------|---------|----------| | HTTP Polling | Client requests every N seconds | High (up to N sec) | High (repeated headers) | | Long Polling | Client waits for server response | Medium | Medium | | Server-Sent Events | Server pushes (one-way) | Low | Low | | WebSocket | Full-duplex, persistent | Very low | Very low |
WebSocket provides a persistent, bidirectional channel between client and server — perfect for streaming sensor data to dashboards in real-time.
WebSocket Protocol Basics
Handshake
WebSocket starts as an HTTP request that gets "upgraded":
Client → Server:
GET /ws HTTP/1.1
Host: dashboard.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server → Client:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After this handshake, the connection stays open for bidirectional messaging.
Frame Format
WebSocket messages are lightweight frames:
0 1 2 3 4 5 6 7
+-+-+-+-+---------+
|F|R|R|R| Opcode | 1 byte
|I|S|S|S| |
|N|V|V|V| |
+-+-+-+-+---------+
|M| Payload Len | 1 byte
|A| |
|S| |
|K| |
+-+---------------+
| Payload Data | Variable
+─────────────────+
Total overhead: 2-14 bytes per message (vs ~800+ bytes for HTTP headers).
Architecture: MQTT + WebSocket Bridge
The most common IoT dashboard architecture:
[IoT Sensors] ──MQTT──→ [MQTT Broker] ──→ [Bridge Service] ──WS──→ [Browser Dashboard]
(Mosquitto) (Node.js) (React/Vue/Vanilla)
│
└──→ [Database] (InfluxDB/TimescaleDB)
│
[Historical data API]
Why Not Direct WebSocket from Sensors?
- IoT devices use MQTT (lightweight, QoS, retained messages)
- Browsers use WebSocket (native support, no MQTT library needed)
- The bridge service translates between the two protocols
- This separation allows scaling each component independently
Implementation
Backend: Node.js WebSocket Server with MQTT Bridge
// server.js
import { WebSocketServer } from 'ws';
import mqtt from 'mqtt';
// Connect to MQTT broker
const mqttClient = mqtt.connect('mqtt://localhost:1883');
// Create WebSocket server
const wss = new WebSocketServer({ port: 8080 });
// Store connected clients and their subscriptions
const clients = new Map();
mqttClient.on('connect', () => {
console.log('Connected to MQTT broker');
// Subscribe to all sensor topics
mqttClient.subscribe('sensors/#');
});
// When MQTT message arrives, forward to WebSocket clients
mqttClient.on('message', (topic, message) => {
const data = JSON.stringify({
topic,
payload: JSON.parse(message.toString()),
timestamp: Date.now()
});
// Broadcast to all connected WebSocket clients
wss.clients.forEach((client) => {
if (client.readyState === 1) { // OPEN
client.send(data);
}
});
});
// Handle WebSocket connections
wss.on('connection', (ws) => {
console.log('Dashboard client connected');
// Handle messages from dashboard (e.g., commands to devices)
ws.on('message', (message) => {
const { topic, payload } = JSON.parse(message);
// Forward command to MQTT
mqttClient.publish(topic, JSON.stringify(payload));
});
ws.on('close', () => {
console.log('Dashboard client disconnected');
});
});
console.log('WebSocket server running on ws://localhost:8080');Frontend: Real-Time Dashboard
<!DOCTYPE html>
<html>
<head>
<title>IoT Dashboard</title>
</head>
<body>
<div id="sensors"></div>
<script>
const ws = new WebSocket('ws://localhost:8080');
const sensorData = {};
ws.onopen = () => {
console.log('Connected to dashboard server');
};
ws.onmessage = (event) => {
const { topic, payload, timestamp } = JSON.parse(event.data);
// Update sensor data
sensorData[topic] = { ...payload, lastUpdate: timestamp };
// Render dashboard
renderDashboard();
};
ws.onclose = () => {
console.log('Disconnected — reconnecting...');
setTimeout(() => location.reload(), 3000);
};
function renderDashboard() {
const container = document.getElementById('sensors');
container.innerHTML = Object.entries(sensorData)
.map(([topic, data]) => `
<div class="sensor-card">
<h3>${topic}</h3>
<p>Temperature: ${data.temperature}°C</p>
<p>Humidity: ${data.humidity}%</p>
<small>Updated: ${new Date(data.lastUpdate).toLocaleTimeString()}</small>
</div>
`).join('');
}
// Send command to device
function sendCommand(topic, payload) {
ws.send(JSON.stringify({ topic, payload }));
}
</script>
</body>
</html>Using MQTT over WebSocket Directly
Some MQTT brokers (like Mosquitto) support WebSocket transport natively:
# mosquitto.conf
listener 1883 # Standard MQTT
listener 9001 # MQTT over WebSocket
protocol websockets
// Browser connects directly to MQTT broker via WebSocket
import mqtt from 'mqtt/dist/mqtt.min';
const client = mqtt.connect('ws://broker.example.com:9001');
client.on('connect', () => {
client.subscribe('sensors/#');
});
client.on('message', (topic, message) => {
const data = JSON.parse(message.toString());
updateDashboard(topic, data);
});Scaling WebSocket Connections
Connection Limits
A single server can handle tens of thousands of WebSocket connections, but scaling requires:
Horizontal Scaling with Redis PubSub
[Browser] ──WS──→ [WS Server 1] ──→ [Redis PubSub] ←── [WS Server 2] ←──WS── [Browser]
[Browser] ──WS──→ [WS Server 1] ↑ [WS Server 2] ←──WS── [Browser]
[MQTT Bridge]
Redis PubSub ensures messages reach all WebSocket servers, regardless of which server a client is connected to.
Load Balancing
Use sticky sessions (IP hash) or WebSocket-aware load balancers:
# nginx WebSocket proxy
upstream websocket {
ip_hash; # Sticky sessions
server ws1.internal:8080;
server ws2.internal:8080;
}
server {
listen 443 ssl;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400; # Keep connection alive
}
}Security
Always Use WSS (WebSocket Secure)
// Use wss:// (TLS) instead of ws://
const ws = new WebSocket('wss://dashboard.example.com/ws');Authentication
// Token-based authentication
const ws = new WebSocket('wss://dashboard.example.com/ws');
ws.onopen = () => {
// Send auth token as first message
ws.send(JSON.stringify({
type: 'auth',
token: 'your-jwt-token'
}));
};
// Server-side: validate token before accepting messages
wss.on('connection', (ws, req) => {
ws.isAuthenticated = false;
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'auth') {
ws.isAuthenticated = verifyJWT(data.token);
return;
}
if (!ws.isAuthenticated) {
ws.close(4001, 'Unauthorized');
return;
}
// Process authenticated message...
});
});Conclusion
WebSocket is the ideal protocol for real-time IoT dashboards. Combined with MQTT for device communication and a bridge service, you can build responsive monitoring systems that display live sensor data with minimal latency. Whether you're monitoring railway equipment, industrial sensors, or smart building systems, this architecture scales from a few devices to thousands.
Related: IoT Real-Time Data Transfer, IoT Connectivity Protocols Compared, and Building REST APIs.