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───┘
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)}
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)
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>
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"]
Performance Tips
- Batch updates: Don't send every metric individually — batch into 1-second intervals
- Delta compression: Only send changed values
- Connection pooling: Reuse WebSocket connections
- Binary frames: Use MessagePack instead of JSON for large payloads
- 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
Top comments (0)