DEV Community

Anna lilith
Anna lilith

Posted on

Build a Real-Time Dashboard with Python, FastAPI, and WebSockets

Build a Real-Time Dashboard with Python, FastAPI, and WebSockets

Static dashboards are dead. Users expect live data streaming directly to their browser. Here's how to build one with Python.

Why Real-Time Dashboards?

  • Monitoring: See server metrics as they happen
  • Analytics: Track user behavior in real-time
  • Trading: Display live price feeds
  • Collaboration: Multiple users editing simultaneously
  • IoT: Visualize sensor data streams

Architecture

Browser ──WebSocket──▶ FastAPI ──▶ Redis Pub/Sub ──▶ Data Workers
   │                       │
   └──Chart.js/Plotly.js───┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Backend with WebSockets

# main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
import asyncio
import json
import redis.asyncio as aioredis

app = FastAPI()

# Connection manager
class ConnectionManager:
    def __init__(self):
        self.active: list[WebSocket] = []

    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.active.append(ws)

    def disconnect(self, ws: WebSocket):
        self.active.remove(ws)

    async def broadcast(self, data: dict):
        message = json.dumps(data)
        for ws in self.active[:]:
            try:
                await ws.send_text(message)
            except:
                self.active.remove(ws)

manager = ConnectionManager()

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Start background data stream
    task = asyncio.create_task(data_stream())
    yield
    task.cancel()

app.router.lifespan_context = lifespan

async def data_stream():
    """Simulate real-time data - replace with your actual data source"""
    import random
    while True:
        data = {
            "timestamp": asyncio.get_event_loop().time(),
            "metrics": {
                "cpu": random.uniform(20, 80),
                "memory": random.uniform(40, 90),
                "requests": random.randint(100, 500),
                "errors": random.randint(0, 5),
            }
        }
        await manager.broadcast(data)
        await asyncio.sleep(1)

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
    await manager.connect(ws)
    try:
        while True:
            # Keep connection alive, handle client messages
            data = await ws.receive_text()
            # Process client commands if needed
    except WebSocketDisconnect:
        manager.disconnect(ws)

@app.get("/health")
async def health():
    return {"status": "healthy", "connections": len(manager.active)}
Enter fullscreen mode Exit fullscreen mode

Step 2: Redis Pub/Sub for Scalability

# redis_stream.py
import asyncio
import redis.asyncio as aioredis
import json

redis = aioredis.from_url("redis://localhost:6379")

async def publish_metrics(data: dict):
    await redis.publish("metrics", json.dumps(data))

async def subscribe_metrics(callback):
    pubsub = redis.pubsub()
    await pubsub.subscribe("metrics")
    async for message in pubsub.listen():
        if message["type"] == "message":
            data = json.loads(message["data"])
            await callback(data)
Enter fullscreen mode Exit fullscreen mode

Step 3: Frontend Dashboard

<!DOCTYPE html>
<html>
<head>
    <title>Real-Time Dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body { font-family: -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; margin: 0; }
        .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; padding: 20px; }
        .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; }
        .metric { font-size: 2.5em; font-weight: bold; }
        .metric-label { color: #8b949e; font-size: 0.9em; }
        .status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
        .status.connected { background: #3fb950; }
        .status.disconnected { background: #f85149; }
    </style>
</head>
<body>
    <div class="grid">
        <div class="card">
            <h3>CPU Usage <span class="status" id="status"></span></h3>
            <div class="metric" id="cpu">--</div>
            <div class="metric-label">% utilized</div>
            <canvas id="cpuChart"></canvas>
        </div>
        <div class="card">
            <h3>Memory Usage</h3>
            <div class="metric" id="memory">--</div>
            <div class="metric-label">% utilized</div>
            <canvas id="memChart"></canvas>
        </div>
        <div class="card">
            <h3>Requests/sec</h3>
            <div class="metric" id="requests">--</div>
            <div class="metric-label">active requests</div>
        </div>
        <div class="card">
            <h3>Error Rate</h3>
            <div class="metric" id="errors">--</div>
            <div class="metric-label">errors per second</div>
        </div>
    </div>

    <script>
        const cpuHistory = [];
        const memHistory = [];

        const cpuChart = new Chart(document.getElementById('cpuChart'), {
            type: 'line',
            data: { labels: [], datasets: [{ data: [], borderColor: '#58a6ff', tension: 0.5 }] },
            options: { plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { min: 0, max: 100 } } }
        });

        const memChart = new Chart(document.getElementById('memChart'), {
            type: 'line',
            data: { labels: [], datasets: [{ data: [], borderColor: '#f0883e', tension: 0.5 }] },
            options: { plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { min: 0, max: 100 } } }
        });

        function connect() {
            const ws = new WebSocket(`ws://${location.host}/ws`);
            const statusEl = document.getElementById('status');

            ws.onopen = () => { statusEl.className = 'status connected'; };
            ws.onclose = () => { statusEl.className = 'status disconnected'; setTimeout(connect, 3000); };

            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                const m = data.metrics;

                document.getElementById('cpu').textContent = m.cpu.toFixed(1);
                document.getElementById('memory').textContent = m.memory.toFixed(1);
                document.getElementById('requests').textContent = m.requests;
                document.getElementById('errors').textContent = m.errors;

                const now = new Date().toLocaleTimeString();
                cpuChart.data.labels.push(now);
                cpuChart.data.datasets[0].data.push(m.cpu);
                if (cpuChart.data.labels.length > 30) { cpuChart.data.labels.shift(); cpuChart.data.datasets[0].data.shift(); }
                cpuChart.update('none');

                memChart.data.labels.push(now);
                memChart.data.datasets[0].data.push(m.memory);
                if (memChart.data.labels.length > 30) { memChart.data.labels.shift(); memChart.data.datasets[0].data.shift(); }
                memChart.update('none');
            };
        }

        connect();
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 4: Docker Compose

version: '3.8'
services:
  dashboard:
    build: .
    ports: ["8080:8080"]
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on: [redis]

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]
Enter fullscreen mode Exit fullscreen mode

Performance Tips

  1. Batch updates: Don't send every metric individually — batch into 1-second intervals
  2. Delta compression: Only send changed values
  3. Connection pooling: Reuse WebSocket connections
  4. Binary frames: Use MessagePack instead of JSON for large payloads
  5. Server-Sent Events: Consider SSE instead of WebSocket for one-way data

Production Considerations

  • Add authentication to WebSocket connections
  • Implement connection limits per user
  • Use Redis Pub/Sub for multi-server broadcasting
  • Add graceful shutdown handling
  • Monitor WebSocket connection counts
  • Implement reconnection logic on the client

Real-time dashboards transform how users interact with data. The investment in WebSockets pays off in user engagement and satisfaction.


Get the Production-Ready Version

Don't want to build it yourself? We have production-ready versions at https://petroleum-board-hawaii-lol.trycloudflare.com.

What you get:

  • Complete, tested Python code
  • Documentation and setup guides
  • Instant delivery after crypto payment
  • Free updates

Browse the collection →

Top comments (0)