Skip to main content
โšก Calmops

Building Real-Time Dashboards: WebSocket, Server-Sent Events, and Polling

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

  1. Choose the right technology

    • Polling: Simple needs, 5+ second latency acceptable
    • SSE: One-way server push, firewall-friendly
    • WebSocket: Bidirectional, sub-second latency required
  2. Handle failures gracefully

    • Implement auto-reconnection with exponential backoff
    • Queue messages during disconnection
    • Show connection status to users
  3. 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
  4. Plan for scale

    • Use Redis pub/sub for multi-server deployments
    • Implement connection limiting and cleanup
    • Monitor active connections per server
  5. 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

Comments