Introduction
Real-time dashboards keep users informed with live data updates across finance, monitoring, analytics, and operations. The challenge is choosing the right technology for your specific needsโeach approach has distinct trade-offs in latency, complexity, scalability, and resource usage.
This guide covers building production-ready real-time dashboards with practical implementation patterns, architecture considerations, and scaling strategies.
Understanding Real-Time Communication Methods
When to Use Each Approach
| Method | Latency | Connections | Direction | Best For |
|---|---|---|---|---|
| Polling | Seconds | Standard HTTP | Client-pull | Infrequent updates, simple needs |
| Long Polling | Sub-second | Held open | Client-pull | Moderate real-time needs |
| SSE | Sub-second | One-way | Server-push | Live feeds, notifications |
| WebSocket | Milliseconds | Full-duplex | Bidirectional | Trading, collaboration, gaming |
Polling: Simple and Reliable
Polling is the easiest to implement and works well when second-level latency is acceptable.
Basic Polling Implementation
// Simple polling with exponential backoff
class PollingClient {
constructor(url, options = {}) {
this.url = url;
this.interval = options.interval || 5000;
this.maxInterval = options.maxInterval || 60000;
this.onData = options.onData || (() => {});
this.running = false;
this.currentInterval = this.interval;
}
async poll() {
if (!this.running) return;
try {
const response = await fetch(this.url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
this.onData(data);
// Reset interval on success
this.currentInterval = this.interval;
} catch (error) {
// Exponential backoff on error
this.currentInterval = Math.min(
this.currentInterval * 2,
this.maxInterval
);
console.error('Polling error:', error.message);
}
// Schedule next poll
setTimeout(() => this.poll(), this.currentInterval);
}
start() {
this.running = true;
this.poll();
}
stop() {
this.running = false;
}
}
// Usage
const client = new PollingClient('/api/metrics', {
interval: 5000,
onData: (data) => updateDashboard(data)
});
client.start();
Smart Polling with Data Hashing
// Only update if data changed
class SmartPollingClient {
constructor(url, onUpdate) {
this.url = url;
this.onUpdate = onUpdate;
this.lastETag = null;
this.lastData = null;
}
async poll() {
const headers = {};
if (this.lastETag) {
headers['If-None-Match'] = this.lastETag;
}
const response = await fetch(this.url, { headers });
if (response.status === 304) {
// Data unchanged, skip processing
return;
}
this.lastETag = response.headers.get('ETag');
const data = await response.json();
// Deep compare to avoid unnecessary updates
if (JSON.stringify(data) !== JSON.stringify(this.lastData)) {
this.lastData = data;
this.onUpdate(data);
}
}
start(interval = 5000) {
setInterval(() => this.poll(), interval);
}
}
Server-Sent Events (SSE)
SSE provides a simple way to push updates from server to client over HTTP. Perfect for live feeds, notifications, and one-way data streams.
Flask SSE Implementation
from flask import Flask, Response, stream_with_context
import json
import time
import random
from threading import Lock
app = Flask(__name__)
# Simple event emitter
class EventEmitter:
def __init__(self):
self.clients = []
self.lock = Lock()
def subscribe(self, queue):
with self.lock:
self.clients.append(queue)
def unsubscribe(self, queue):
with self.lock:
if queue in self.clients:
self.clients.remove(queue)
def emit(self, event_type, data):
message = f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
with self.lock:
dead_clients = []
for client in self.clients:
try:
client.put(message)
except:
dead_clients.append(client)
for client in dead_clients:
self.clients.remove(client)
emitter = EventEmitter()
@app.route('/events')
def events():
from queue import Queue
queue = Queue()
emitter.subscribe(queue)
def generate():
try:
while True:
message = queue.get(timeout=30)
yield message
except:
pass
finally:
emitter.unsubscribe(queue)
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
# Emit events from your application
@app.route('/api/update-metrics')
def update_metrics():
metrics = {
'cpu': random.randint(0, 100),
'memory': random.randint(20, 80),
'timestamp': time.time()
}
emitter.emit('metrics', metrics)
return {'status': 'emitted'}
JavaScript SSE Client with Reconnection
class SSEClient {
constructor(url, options = {}) {
this.url = url;
this.onMessage = options.onMessage || (() => {});
this.onError = options.onError || (() => {});
this.reconnectDelay = options.reconnectDelay || 1000;
this.maxReconnectDelay = options.maxReconnectDelay || 30000;
this.eventSource = null;
this.reconnectTimer = null;
}
connect() {
this.eventSource = new EventSource(this.url);
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data, event.type);
} catch (e) {
console.error('Failed to parse SSE data:', e);
}
};
this.eventSource.onerror = (error) => {
this.onError(error);
this.scheduleReconnect();
};
// Named event handlers
this.eventSource.addEventListener('metrics', (event) => {
const data = JSON.parse(event.data);
this.onMessage(data, 'metrics');
});
this.eventSource.addEventListener('alert', (event) => {
const data = JSON.parse(event.data);
this.onMessage(data, 'alert');
});
}
scheduleReconnect() {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, this.reconnectDelay);
// Exponential backoff
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
}
// Usage
const sse = new SSEClient('/events', {
onMessage: (data, eventType) => {
console.log(`Received ${eventType}:`, data);
updateDashboard(data);
},
onError: (error) => {
console.error('SSE error:', error);
}
});
sse.connect();
WebSocket: Full-Duplex Communication
WebSockets provide bidirectional, low-latency communication ideal for trading platforms, collaborative apps, and high-frequency updates.
Python WebSocket Server with asyncio
import asyncio
import json
import websockets
from datetime import datetime
import random
# Connected clients
connected_clients = set()
async def register(websocket):
connected_clients.add(websocket)
print(f"Client connected. Total: {len(connected_clients)}")
async def unregister(websocket):
connected_clients.discard(websocket)
print(f"Client disconnected. Total: {len(connected_clients)}")
async def broadcast(message):
"""Broadcast message to all connected clients."""
if connected_clients:
await asyncio.gather(
*[client.send(json.dumps(message)) for client in connected_clients],
return_exceptions=True
)
async def handle_client(websocket, path):
await register(websocket)
try:
async for message in websocket:
data = json.loads(message)
# Handle different message types
if data.get('type') == 'subscribe':
# Handle subscription
channel = data.get('channel')
await websocket.send(json.dumps({
'type': 'subscribed',
'channel': channel
}))
elif data.get('type') == 'ping':
await websocket.send(json.dumps({'type': 'pong'}))
else:
# Echo back for demonstration
await websocket.send(json.dumps({
'type': 'ack',
'original': data
}))
except websockets.exceptions.ConnectionClosed:
pass
finally:
await unregister(websocket)
# Background task to broadcast metrics
async def broadcast_metrics():
while True:
metrics = {
'type': 'metrics',
'data': {
'cpu': random.randint(0, 100),
'memory': random.randint(20, 80),
'timestamp': datetime.utcnow().isoformat()
}
}
await broadcast(metrics)
await asyncio.sleep(1)
async def main():
async with websockets.serve(handle_client, "localhost", 8765):
# Start broadcasting metrics
asyncio.create_task(broadcast_metrics())
print("WebSocket server started on ws://localhost:8765")
await asyncio.Future() # Run forever
asyncio.run(main())
JavaScript WebSocket Client with Auto-Reconnection
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.reconnectDelay = options.reconnectDelay || 1000;
this.messageQueue = [];
this.subscriptions = new Set();
}
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.reconnectDelay = this.options.reconnectDelay || 1000;
// Resubscribe to previous subscriptions
for (const channel of this.subscriptions) {
this.send({ type: 'subscribe', channel });
}
// Send queued messages
while (this.messageQueue.length > 0) {
const msg = this.messageQueue.shift();
this.ws.send(JSON.stringify(msg));
}
resolve();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (this.options.onMessage) {
this.options.onMessage(data);
}
// Handle system messages
if (data.type === 'subscribed') {
console.log(`Subscribed to ${data.channel}`);
}
} catch (e) {
console.error('Failed to parse message:', e);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
if (this.options.onError) {
this.options.onError(error);
}
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.attemptReconnect();
};
});
}
attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect().catch(console.error);
}, delay);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
// Queue message for when connection is restored
this.messageQueue.push(data);
}
}
subscribe(channel) {
this.subscriptions.add(channel);
this.send({ type: 'subscribe', channel });
}
disconnect() {
this.maxReconnectAttempts = 0; // Prevent reconnection
if (this.ws) {
this.ws.close();
}
}
}
// Usage
const ws = new WebSocketClient('ws://localhost:8765', {
onMessage: (data) => {
console.log('Received:', data);
updateDashboard(data.data);
},
onError: (error) => {
console.error('Connection error:', error);
}
});
ws.connect();
ws.subscribe('metrics');
Dashboard Architecture Patterns
Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Data Sources โ
โ (Database, API, Message Queue, IoT Devices) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Backend Services โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Aggregator โ โ Queue โ โ WebSocket Server โ โ
โ โ Service โ โ (Kafka) โ โ (Connections) โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โผ โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โ SSE โ โ WebSocketโ โ REST โ
โ Endpoint โ โ Endpoint โ โ API โ
โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend Client โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Charts โ โ Tables โ โ Notifications โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Backend: Data Aggregation Service
# aggregator.py
import asyncio
from datetime import datetime
from typing import Dict, List
import json
class MetricsAggregator:
def __init__(self):
self.cache = {}
self.subscribers = []
async def get_latest_metrics(self) -> Dict:
"""Aggregate latest metrics from all sources."""
results = await asyncio.gather(
self.get_database_metrics(),
self.get_api_metrics(),
self.get_infrastructure_metrics(),
self.get_business_metrics()
)
return {
'database': results[0],
'api': results[1],
'infrastructure': results[2],
'business': results[3],
'timestamp': datetime.utcnow().isoformat()
}
async def get_database_metrics(self) -> Dict:
# Simulated database metrics
return {
'connections': 150,
'queries_per_second': 1200,
'slow_queries': 3,
'cache_hit_ratio': 0.95
}
async def get_api_metrics(self) -> Dict:
return {
'requests_per_second': 500,
'avg_response_time': 45,
'error_rate': 0.002,
'active_users': 10000
}
async def get_infrastructure_metrics(self) -> Dict:
return {
'cpu_usage': 45,
'memory_usage': 60,
'disk_usage': 40,
'network_in': 1000000,
'network_out': 500000
}
async def get_business_metrics(self) -> Dict:
return {
'orders_today': 1250,
'revenue_today': 45000,
'new_users_today': 150,
'conversion_rate': 0.035
}
def subscribe(self, callback):
"""Subscribe to metric updates."""
self.subscribers.append(callback)
async def start_broadcasting(self, interval=1):
"""Periodically broadcast metrics to subscribers."""
while True:
metrics = await self.get_latest_metrics()
for subscriber in self.subscribers:
try:
await subscriber(metrics)
except Exception as e:
print(f"Subscriber error: {e}")
await asyncio.sleep(interval)
# Usage
aggregator = MetricsAggregator()
# Add websocket broadcaster
async def broadcast_to_websocket(metrics):
await broadcast(json.dumps({'type': 'metrics', 'data': metrics}))
aggregator.subscribe(broadcast_to_websocket)
# Start
asyncio.run(aggregator.start_broadcasting())
Frontend: Dashboard Component
// React Dashboard Component
import React, { useState, useEffect, useRef } from 'react';
import { LineChart, BarChart } from 'charting-library';
import { WebSocketClient } from './websocket-client';
export function Dashboard() {
const [metrics, setMetrics] = useState(null);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const wsRef = useRef(null);
const chartRef = useRef(null);
const dataBuffer = useRef([]);
useEffect(() => {
// Initialize WebSocket connection
wsRef.current = new WebSocketClient('wss://api.example.com/ws', {
onMessage: (data) => {
if (data.type === 'metrics') {
handleNewMetrics(data.data);
}
},
onError: (error) => {
setConnectionStatus('error');
}
});
wsRef.current.connect().then(() => {
setConnectionStatus('connected');
});
return () => {
wsRef.current?.disconnect();
};
}, []);
const handleNewMetrics = (newData) => {
setMetrics(newData);
// Buffer data for charts
dataBuffer.current.push({
timestamp: newData.timestamp,
cpu: newData.infrastructure.cpu_usage,
memory: newData.infrastructure.memory_usage
});
// Keep only last 60 data points
if (dataBuffer.current.length > 60) {
dataBuffer.current = dataBuffer.current.slice(-60);
}
};
return (
<div className="dashboard">
<header>
<h1>Real-Time Dashboard</h1>
<ConnectionIndicator status={connectionStatus} />
</header>
<div className="metrics-grid">
<MetricCard
title="CPU Usage"
value={metrics?.infrastructure?.cpu_usage}
unit="%"
trend="up"
/>
<MetricCard
title="Memory"
value={metrics?.infrastructure?.memory_usage}
unit="%"
trend="stable"
/>
<MetricCard
title="Requests/sec"
value={metrics?.api?.requests_per_second}
unit="req/s"
trend="up"
/>
<MetricCard
title="Error Rate"
value={(metrics?.api?.error_rate * 100).toFixed(2)}
unit="%"
trend="down"
/>
</div>
<div className="charts">
<LineChart
ref={chartRef}
data={dataBuffer.current}
xKey="timestamp"
yKeys={['cpu', 'memory']}
title="System Resources"
/>
</div>
</div>
);
}
function ConnectionIndicator({ status }) {
const colors = {
connected: '#10b981',
disconnected: '#f59e0b',
error: '#ef4444'
};
return (
<div className="connection-indicator">
<span
className="indicator"
style={{ backgroundColor: colors[status] }}
/>
<span>{status}</span>
</div>
);
}
Performance Considerations
Connection Management
# Nginx configuration for WebSocket support
server {
# ... other config ...
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering off;
proxy_cache off;
}
# SSE endpoint
location /events/ {
proxy_pass http://backend;
proxy_http_version 1.1;
# SSE requires chunked transfer
proxy_buffering off;
proxy_cache off;
# Headers
proxy_set_header X-Accel-Buffering no;
}
}
Scaling WebSockets
# Redis pub/sub for multi-server WebSocket scaling
import aioredis
import asyncio
class ScaledWebSocketManager:
def __init__(self, redis_url):
self.redis = None
self.redis_url = redis_url
self.pubsub = None
self.channel = 'dashboard:metrics'
async def connect(self):
self.redis = await aioredis.create_redis_pool(self.redis_url)
self.pubsub = await self.redis.subscribe(self.channel)
# Listen for messages from other servers
asyncio.create_task(self._listen())
async def _listen(self):
async for message in self.pubsub:
data = message.decode()
await self.broadcast_to_local_clients(data)
async def publish(self, data):
"""Publish metrics to all server instances."""
await self.redis.publish(self.channel, data)
async def broadcast_to_local_clients(self, data):
"""Override to broadcast to local WebSocket connections."""
pass
Best Practices Summary
-
Choose the right technology
- Polling: Simple needs, 5+ second latency acceptable
- SSE: One-way server push, firewall-friendly
- WebSocket: Bidirectional, sub-second latency required
-
Handle failures gracefully
- Implement auto-reconnection with exponential backoff
- Queue messages during disconnection
- Show connection status to users
-
Optimize for the client
- Throttle updates to match display refresh rate (60fps)
- Use data hashing (ETag) to avoid unnecessary updates
- Implement client-side buffering for charts
-
Plan for scale
- Use Redis pub/sub for multi-server deployments
- Implement connection limiting and cleanup
- Monitor active connections per server
-
Consider the user experience
- Show last-known data when disconnected
- Provide clear loading and error states
- Test on mobile devices (battery impact)
Conclusion
Building real-time dashboards requires careful consideration of your specific needs. For simple displays with minute-level updates, polling is sufficient. When you need server-push over HTTP, SSE provides an elegant solution. For high-frequency, bidirectional communication, WebSockets are the answer.
The key to successful real-time dashboards is matching the technology to your requirementsโnot overengineering with WebSockets when polling would work, but not accepting poor user experience when real-time is critical.
Resources
- WebSocket API - MDN - WebSocket client and server API
- Server-Sent Events - MDN - SSE implementation guide
- WebSockets with FastAPI - Python WebSocket tutorial
- Socket.io - WebSocket abstraction library with fallbacks
- SSE vs WebSocket Comparison - Detailed comparison
- Chart.js - JavaScript charting library for dashboards
Comments