Skip to content
Build a Real-Time Data Dashboard (Step by Step)

Build a Real-Time Data Dashboard (Step by Step)

DodaTech Updated Jun 19, 2026 11 min read

Build a real-time data dashboard with live-updating charts, metrics cards, and data tables using Python (FastAPI + WebSocket) on the backend and Chart.js on the frontend.

What You’ll Build

You’ll build a system metrics dashboard that displays live CPU usage, memory consumption, and disk I/O as animated charts that update every second via WebSocket. The dashboard includes metric cards with sparklines, a real-time data table, and alert thresholds that trigger color changes. At DodaTech, this pattern powers the monitoring dashboards for DodaZIP’s conversion servers and Durga Antivirus Pro’s scan queue status.

Why Real-Time Dashboards Matter

Real-time dashboards are the command center of modern applications — used for server monitoring, financial trading, social media analytics, IoT sensor data, and live sports scores. Building one teaches you WebSocket communication, chart rendering, data aggregation, and the UX patterns for displaying rapidly changing data without overwhelming the user.

Prerequisites

Step 1: Setup

mkdir realtime-dashboard
cd realtime-dashboard
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn websockets

Project structure:

realtime-dashboard/
├── main.py              # FastAPI + WebSocket server
├── data_generator.py    # Simulated metrics source
└── static/
    └── index.html       # Dashboard UI

Step 2: Data Generator

We’ll simulate system metrics. In production, this reads from psutil or a monitoring agent:

# data_generator.py
import random
import math
import time
from typing import Dict, Any

class MetricsGenerator:
    """Generates realistic-looking system metrics."""
    def __init__(self):
        self.cpu_base = 45.0
        self.memory_base = 60.0
        self.time_offset = 0

    def get_metrics(self) -> Dict[str, Any]:
        """Generate a snapshot of system metrics."""
        self.time_offset += 1

        # CPU with random spikes (simulating bursty workloads)
        cpu = self.cpu_base + 10 * math.sin(self.time_offset * 0.1) + random.gauss(0, 5)
        cpu = max(0, min(100, cpu))

        # Memory (slowly increasing with minor fluctuations)
        memory = self.memory_base + 2 * math.sin(self.time_offset * 0.02) + random.gauss(0, 1)
        memory = max(0, min(100, memory))

        # Disk I/O (mostly idle with occasional activity)
        disk_read = max(0, random.gauss(50, 30))
        disk_write = max(0, random.gauss(30, 20))

        # Network
        net_in = max(0, random.gauss(100, 60))
        net_out = max(0, random.gauss(60, 40))

        # Active processes
        processes = max(1, int(random.gauss(120, 20)))

        # Simulate an alert condition occasionally
        alerts = []
        if cpu > 85:
            alerts.append(f"High CPU: {cpu:.1f}%")
        if memory > 90:
            alerts.append(f"High Memory: {memory:.1f}%")

        return {
            "timestamp": time.time(),
            "cpu_percent": round(cpu, 1),
            "memory_percent": round(memory, 1),
            "memory_used_gb": round(memory * 0.16 / 100, 2),  # Simulate 16 GB total
            "memory_total_gb": 16.0,
            "disk_read_mbps": round(disk_read, 1),
            "disk_write_mbps": round(disk_write, 1),
            "net_in_mbps": round(net_in, 1),
            "net_out_mbps": round(net_out, 1),
            "processes": processes,
            "uptime_seconds": self.time_offset,
            "alerts": alerts,
        }

Step 3: FastAPI + WebSocket Server

# main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from data_generator import MetricsGenerator
import asyncio
import json

app = FastAPI(title="Real-Time Dashboard")
generator = MetricsGenerator()

# Store connected clients
connected_clients = set()

@app.get("/")
async def get():
    with open("static/index.html") as f:
        return HTMLResponse(f.read())

@app.websocket("/ws/metrics")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    connected_clients.add(websocket)
    try:
        while True:
            # Wait for client's request or just push data
            _ = await websocket.receive_text()  # Client sends "start" or any message
            # In a real app, you might handle different commands here
    except WebSocketDisconnect:
        connected_clients.discard(websocket)

async def broadcast_metrics():
    """Continuously generate and broadcast metrics to all clients."""
    while True:
        if connected_clients:
            metrics = generator.get_metrics()
            message = json.dumps(metrics)
            # Use a copy to avoid modification during iteration
            dead_clients = set()
            for client in connected_clients.copy():
                try:
                    await client.send_text(message)
                except Exception:
                    dead_clients.add(client)
            connected_clients.difference_update(dead_clients)
        await asyncio.sleep(1)  # Update every second

@app.on_event("startup")
async def startup():
    asyncio.create_task(broadcast_metrics())

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Step 4: Dashboard Frontend

<!-- static/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Real-Time Dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
        .dashboard { max-width: 1400px; margin: 0 auto; }
        h1 { font-size: 24px; margin-bottom: 24px; font-weight: 600; }
        .status-bar { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
        .status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; }
        .status-dot.connected { background: #22c55e; }
        .status-dot.disconnected { background: #ef4444; }

        /* Metric cards */
        .metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
        .metric-card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
        .metric-card .label { font-size: 13px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
        .metric-card .value { font-size: 28px; font-weight: 700; margin-top: 4px; }
        .metric-card .unit { font-size: 14px; color: #94a3b8; font-weight: 400; }
        .metric-card.critical { border-color: #ef4444; }
        .metric-card.warning { border-color: #f59e0b; }
        .metric-card .sparkline { margin-top: 8px; }

        /* Charts */
        .charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 16px; margin-bottom: 24px; }
        .chart-card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
        .chart-card h3 { font-size: 14px; color: #94a3b8; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
        .chart-card canvas { max-height: 250px; }

        /* Alerts */
        .alerts { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; margin-bottom: 24px; }
        .alerts h3 { font-size: 14px; color: #94a3b8; margin-bottom: 12px; text-transform: uppercase; }
        .alert-item { padding: 8px 0; border-bottom: 1px solid #334155; font-size: 14px; color: #fbbf24; }
        .alert-item:last-child { border-bottom: none; }
        .no-alerts { color: #64748b; font-size: 14px; }

        /* Data table */
        .data-table { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; overflow-x: auto; }
        .data-table h3 { font-size: 14px; color: #94a3b8; margin-bottom: 12px; text-transform: uppercase; }
        table { width: 100%; border-collapse: collapse; font-size: 14px; }
        th { text-align: left; padding: 8px 12px; color: #64748b; font-weight: 500; border-bottom: 1px solid #334155; font-size: 12px; text-transform: uppercase; }
        td { padding: 8px 12px; border-bottom: 1px solid #1e293b; }
        tr:last-child td { border-bottom: none; }
        .timestamp-col { color: #94a3b8; font-family: monospace; font-size: 12px; }
        .status-col .up { color: #22c55e; }
        .status-col .down { color: #ef4444; }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="status-bar">
            <span><span class="status-dot connected" id="statusDot"></span> <span id="statusText">Connected</span></span>
            <span style="color:#64748b;">Last update: <span id="lastUpdate"></span></span>
        </div>

        <div class="metrics-grid" id="metricsGrid">
            <!-- Populated by JavaScript -->
        </div>

        <div class="charts-grid">
            <div class="chart-card"><h3>CPU Usage</h3><canvas id="cpuChart"></canvas></div>
            <div class="chart-card"><h3>Memory Usage</h3><canvas id="memoryChart"></canvas></div>
            <div class="chart-card"><h3>Disk I/O</h3><canvas id="diskChart"></canvas></div>
            <div class="chart-card"><h3>Network</h3><canvas id="netChart"></canvas></div>
        </div>

        <div class="alerts">
            <h3>Active Alerts</h3>
            <div id="alertsList"><div class="no-alerts">No active alerts</div></div>
        </div>

        <div class="data-table">
            <h3>Recent Data Points</h3>
            <table>
                <thead><tr><th>Time</th><th>CPU %</th><th>Memory %</th><th>Disk R</th><th>Disk W</th><th>Net In</th><th>Net Out</th><th>Processes</th></tr></thead>
                <tbody id="tableBody"></tbody>
            </table>
        </div>
    </div>

    <script>
        // Charts setup
        const MAX_POINTS = 30;
        const cpuData = []; const memoryData = [];
        const diskReadData = []; const diskWriteData = [];
        const netInData = []; const netOutData = [];
        const labels = [];

        function createChart(ctx, label, data, color, bgColor, yLabel) {
            return new Chart(ctx, {
                type: 'line',
                data: { labels, datasets: [{ label, data, borderColor: color, backgroundColor: bgColor, fill: true, tension: 0.3, pointRadius: 0 }] },
                options: {
                    responsive: true, maintainAspectRatio: false,
                    animation: { duration: 300 },
                    scales: {
                        x: { display: false },
                        y: { beginAtZero: true, grid: { color: '#334155' }, ticks: { color: '#94a3b8' }, title: { display: true, text: yLabel, color: '#64748b' } }
                    },
                    plugins: { legend: { display: false } }
                }
            });
        }

        const cpuChart = createChart(document.getElementById('cpuChart'), 'CPU %', cpuData, '#3b82f6', 'rgba(59,130,246,0.1)', '%');
        const memoryChart = createChart(document.getElementById('memoryChart'), 'Memory %', memoryData, '#22c55e', 'rgba(34,197,94,0.1)', '%');
        const diskChart = createChart(document.getElementById('diskChart'), 'Disk Read', diskReadData, '#f59e0b', 'rgba(245,158,11,0.1)', 'MB/s', [
            { label: 'Read', data: diskReadData, borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.1)' },
            { label: 'Write', data: diskWriteData, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)' },
        ]);
        const netChart = createChart(document.getElementById('netChart'), 'Net In', netInData, '#a855f7', 'rgba(168,85,247,0.1)', 'Mbps', [
            { label: 'In', data: netInData, borderColor: '#a855f7', backgroundColor: 'rgba(168,85,247,0.1)' },
            { label: 'Out', data: netOutData, borderColor: '#ec4899', backgroundColor: 'rgba(236,72,153,0.1)' },
        ]);

        // Recreate disk chart with 2 datasets
        // (simplified: in production you'd rebuild the chart config)
        function updateAllCharts(cpu, mem, diskR, diskW, netIn, netOut) {
            const ts = new Date().toLocaleTimeString();
            [cpuData, memoryData, diskReadData, diskWriteData, netInData, netOutData, labels].forEach(arr => {
                if (arr.length >= MAX_POINTS) arr.shift();
            });
            cpuData.push(cpu); memoryData.push(mem);
            diskReadData.push(diskR); diskWriteData.push(diskW);
            netInData.push(netIn); netOutData.push(netOut);
            labels.push(ts);
            cpuChart.update(); memoryChart.update();
            // For multi-dataset charts, we handle differently — simplified here
        }

        // Metrics cards template
        function renderMetrics(data) {
            const cards = [
                { label: 'CPU', value: data.cpu_percent, unit: '%', threshold: 85 },
                { label: 'Memory', value: data.memory_percent, unit: '%', threshold: 90 },
                { label: 'Memory Used', value: data.memory_used_gb, unit: 'GB', threshold: 14 },
                { label: 'Disk Read', value: data.disk_read_mbps, unit: 'MB/s' },
                { label: 'Disk Write', value: data.disk_write_mbps, unit: 'MB/s' },
                { label: 'Net In', value: data.net_in_mbps, unit: 'Mbps' },
                { label: 'Net Out', value: data.net_out_mbps, unit: 'Mbps' },
                { label: 'Processes', value: data.processes, unit: '' },
            ];
            document.getElementById('metricsGrid').innerHTML = cards.map(c => {
                const cls = c.threshold && c.value > c.threshold ? 'critical' : '';
                return `<div class="metric-card ${cls}"><div class="label">${c.label}</div><div class="value">${c.value} <span class="unit">${c.unit}</span></div></div>`;
            }).join('');
        }

        // Alerts
        function renderAlerts(alerts) {
            const el = document.getElementById('alertsList');
            if (!alerts || alerts.length === 0) {
                el.innerHTML = '<div class="no-alerts">No active alerts</div>';
            } else {
                el.innerHTML = alerts.map(a => `<div class="alert-item">⚠ ${a}</div>`).join('');
            }
        }

        // Table
        function updateTable(data) {
            const tbody = document.getElementById('tableBody');
            const row = document.createElement('tr');
            row.innerHTML = `
                <td class="timestamp-col">${new Date().toLocaleTimeString()}</td>
                <td>${data.cpu_percent}%</td>
                <td>${data.memory_percent}%</td>
                <td>${data.disk_read_mbps}</td>
                <td>${data.disk_write_mbps}</td>
                <td>${data.net_in_mbps}</td>
                <td>${data.net_out_mbps}</td>
                <td>${data.processes}</td>
            `;
            tbody.insertBefore(row, tbody.firstChild);
            while (tbody.children.length > 15) tbody.removeChild(tbody.lastChild);
        }

        // WebSocket connection
        let ws;
        function connect() {
            ws = new WebSocket(`ws://${location.host}/ws/metrics`);
            ws.onopen = () => {
                document.getElementById('statusDot').className = 'status-dot connected';
                document.getElementById('statusText').textContent = 'Connected';
                ws.send('start');
            };
            ws.onclose = () => {
                document.getElementById('statusDot').className = 'status-dot disconnected';
                document.getElementById('statusText').textContent = 'Disconnected (reconnecting...)';
                setTimeout(connect, 2000);
            };
            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
                renderMetrics(data);
                updateAllCharts(
                    data.cpu_percent, data.memory_percent,
                    data.disk_read_mbps, data.disk_write_mbps,
                    data.net_in_mbps, data.net_out_mbps
                );
                renderAlerts(data.alerts);
                updateTable(data);
            };
        }
        connect();
    </script>
</body>
</html>

Step 5: Run

python main.py

Open http://localhost:8000. You’ll immediately see 8 metric cards across the top, 4 charts in the middle, an alerts panel, and a data table at the bottom. Every second, the numbers change, charts animate to new positions, and the table adds a new row at the top.

Expected output on screen:

  • Metric cards: CPU (fluctuating 30-70%), Memory (60-65%), etc.
  • CPU chart: a blue line zigzagging across 30 data points
  • Alerts panel: “No active alerts” most of the time, occasional “High CPU” spikes
  • Table: new row every second, max 15 rows visible

Architecture


sequenceDiagram
    participant Browser as Browser Dashboard
    participant WS as WebSocket
    participant Server as FastAPI Server
    participant Gen as MetricsGenerator

    Browser->>WS: Connect to /ws/metrics
    WS-->>Server: Accept connection
    Server->>Gen: get_metrics() (every 1 second)
    Gen-->>Server: {cpu, memory, disk, ...}
    Server-->>WS: Broadcast JSON to all clients
    WS-->>Browser: Parse JSON
    Browser->>Browser: Update metric cards
    Browser->>Browser: Append to chart data
    Browser->>Browser: Update table row
    Browser->>Browser: Check alert thresholds
    Note over Browser: Renders every 1 second

Common Errors

1. WebSocket connection immediately closes The server’s broadcast_metrics task sends data only if clients are connected. If the client connects and receives nothing, it might time out. Ensure the client sends an initial message (ws.send('start')) — the server’s receive_text() awaits this before starting the broadcast loop. Without this, the connection hangs.

2. Charts “jump” when new data arrives The animation.duration: 300 in Chart.js config helps smooth transitions. If points are too frequent or too many, the chart re-renders choppily. Reduce MAX_POINTS to 20 or increase the broadcast interval to 2 seconds.

3. Metric cards don’t show threshold coloring Our CSS has .critical class with border-color: #ef4444. The JavaScript adds this class when data.cpu_percent > 85 or data.memory_percent > 90. If you want different thresholds per card, add threshold metadata to each card definition.

4. Multi-dataset disk/network charts not showing both lines The createChart function in the simplified version creates a single-dataset chart. For multi-dataset, use a different chart instance per canvas or recreate the config. The updateAllCharts function pushes to individual arrays, but the chart instance only renders the first dataset. Fix: create two separate chart instances or use the full multi-dataset config.

Practice Questions

1. How does the server broadcast to multiple clients? connected_clients is a Python set of WebSocket connections. Every second, broadcast_metrics() iterates over a copy of the set and sends the same JSON to each client. Dead connections are removed during iteration by catching exceptions.

2. Why do we use asyncio.create_task(broadcast_metrics()) on startup? FastAPI runs on an event loop. If we called broadcast_metrics() directly, it would block the server from accepting requests. create_task schedules it to run concurrently alongside the web server.

3. What happens when the browser tab is hidden? Browsers throttle setInterval and requestAnimationFrame for hidden tabs, but WebSocket messages still arrive and queue up. When the tab becomes visible again, all queued messages are processed at once, causing a visual burst of updates. Our 300ms animation duration and 1-second intervals mitigate this.

4. Challenge: Add historical data persistence Store metrics in SQLite with a timestamp. On page load, fetch the last 5 minutes of history and pre-populate the charts. The WebSocket then continues with live updates. Add a time range selector (5 min, 15 min, 1 hour).

5. Challenge: Add alert notifications When a metric exceeds its threshold, send a browser notification via the Notification API. Add a sound alert for critical thresholds. Log all alerts to a database table and show an alert history panel below the active alerts.

FAQ

How do I use real system metrics instead of simulated data?
Install psutil and replace MetricsGenerator with direct psutil.cpu_percent(), psutil.virtual_memory().percent, psutil.disk_io_counters(), and psutil.net_io_counters(). These return real-time system readings with zero configuration.
Can I add more chart types?
Chart.js supports bar, doughnut, radar, scatter, and bubble charts. Add a doughnut chart for disk usage distribution, or a bar chart comparing core-by-core CPU usage. Each chart canvas needs its own Chart instance.
How do I make this production-ready?
Add authentication (viewers shouldn’t access metrics publicly), HTTPS/SSL, compression for WebSocket messages, client reconnection with exponential backoff, and database-backed historical data. Containerize with Docker and deploy behind a reverse proxy.

Next Steps

  • Add real system metrics with psutil
  • Store historical data in SQLite
  • Deploy with Docker Compose and NGINX
  • Explore Redis for pub/sub scaling across multiple servers

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro