Building a Live Dashboard With Multi-Stream Subscriptions
A live dashboard subscribes to multiple data streams simultaneously—stock prices, server metrics, user activity—and visualizes them in real-time. The challenge: manage multiple WebSocket connections, keep caches in sync, and render charts without jank. This final article brings together all patterns from the series into a production-grade dashboard (Crater Labs, 2026).
Live Dashboard Architecture
A dashboard typically has:
- Multiple subscriptions: One WebSocket per data domain (stocks, metrics, analytics).
- Cached state: Use React Query to manage each stream's data.
- Chart rendering: Lightweight libraries like Recharts or Chart.js.
- Real-time sync: WebSocket messages update React Query cache.
- Error recovery: If one stream fails, others continue.
Complete Live Dashboard Example
Here's a dashboard tracking server metrics, stock prices, and user analytics:
import React, { useEffect, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { useWebSocket } from './useWebSocket'; // Article 4
import { useBatchedMessages } from './useBatchedMessages'; // Article 8
// Stream 1: Server Metrics (CPU, Memory, Network)
export function useServerMetrics() {
const queryClient = useQueryClient();
const addBatch = useBatchedMessages((batch) => {
queryClient.setQueryData(['metrics'], (oldMetrics) => {
let updated = oldMetrics || [];
batch.forEach((msg) => {
updated = [
...updated.slice(-59), // Keep last 60 data points
{
timestamp: msg.timestamp,
cpu: msg.cpu,
memory: msg.memory,
network: msg.network,
},
];
});
return updated;
});
});
useWebSocket('wss://your-server.example.com/metrics', {
onMessage: (msg) => {
if (msg.type === 'metric') {
addBatch(msg);
}
},
});
return useQuery({
queryKey: ['metrics'],
queryFn: async () => {
const res = await fetch('/api/metrics/last-hour');
return res.json();
},
});
}
// Stream 2: Stock Prices
export function useStockPrices() {
const queryClient = useQueryClient();
useWebSocket('wss://your-server.example.com/stocks', {
onMessage: (msg) => {
if (msg.type === 'price') {
queryClient.setQueryData(['stocks'], (oldStocks) => ({
...oldStocks,
[msg.ticker]: {
price: msg.price,
change: msg.change,
timestamp: msg.timestamp,
},
}));
}
},
});
return useQuery({
queryKey: ['stocks'],
queryFn: async () => {
const res = await fetch('/api/stocks/prices');
return res.json();
},
});
}
// Stream 3: User Activity
export function useUserActivity() {
const queryClient = useQueryClient();
const addBatch = useBatchedMessages((batch) => {
queryClient.setQueryData(['activity'], (oldActivity) => {
let counts = oldActivity || { online: 0, signups: 0, transactions: 0 };
batch.forEach((msg) => {
if (msg.event === 'user-join') counts.online++;
if (msg.event === 'user-leave') counts.online--;
if (msg.event === 'signup') counts.signups++;
if (msg.event === 'transaction') counts.transactions++;
});
return counts;
});
});
useWebSocket('wss://your-server.example.com/activity', {
onMessage: (msg) => {
if (msg.type === 'activity') {
addBatch(msg);
}
},
});
return useQuery({
queryKey: ['activity'],
queryFn: async () => {
const res = await fetch('/api/activity/summary');
return res.json();
},
});
}
// Main Dashboard Component
export function LiveDashboard() {
const { data: metrics, isLoading: metricsLoading } = useServerMetrics();
const { data: stocks, isLoading: stocksLoading } = useStockPrices();
const { data: activity, isLoading: activityLoading } = useUserActivity();
const allLoaded = !metricsLoading && !stocksLoading && !activityLoading;
if (!allLoaded) {
return `<div>`Loading dashboard...`</div>`;
}
return (
`<div style={{ padding: '20px', backgroundColor: '#f5f5f5', minHeight: '100vh' }}>`
`<h1>`Live Dashboard`</h1>`
{/* Row 1: KPIs */}
`<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', marginBottom: '20px' }}>`
`<div style={{ backgroundColor: 'white', padding: '16px', borderRadius: '8px' }}>`
`<h3>`Online Users`</h3>`
`<p style={{ fontSize: '2em', fontWeight: 'bold' }}>`{activity?.online || 0}`</p>`
`</div>`
`<div style={{ backgroundColor: 'white', padding: '16px', borderRadius: '8px' }}>`
`<h3>`Signups Today`</h3>`
`<p style={{ fontSize: '2em', fontWeight: 'bold' }}>`{activity?.signups || 0}`</p>`
`</div>`
`<div style={{ backgroundColor: 'white', padding: '16px', borderRadius: '8px' }}>`
`<h3>`Transactions`</h3>`
`<p style={{ fontSize: '2em', fontWeight: 'bold' }}>`${(activity?.transactions * 99.99).toFixed(2) || '0'}`</p>`
`</div>`
`</div>`
{/* Row 2: Charts */}
`<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}>`
{/* Server Metrics Chart */}
`<div style={{ backgroundColor: 'white', padding: '16px', borderRadius: '8px' }}>`
`<h3>`Server Metrics`</h3>`
`<ResponsiveContainer width="100%" height={300}>`
`<LineChart data={metrics || []}>`
`<XAxis dataKey="timestamp" />`
`<YAxis />`
`<Tooltip />`
`<Legend />`
`<Line type="monotone" dataKey="cpu" stroke="#ff7300" />`
`<Line type="monotone" dataKey="memory" stroke="#0088fe" />`
`</LineChart>`
`</ResponsiveContainer>`
`</div>`
{/* Stock Prices */}
`<div style={{ backgroundColor: 'white', padding: '16px', borderRadius: '8px' }}>`
`<h3>`Stock Prices`</h3>`
`<table style={{ width: '100%' }}>`
`<thead>`
`<tr>`
`<th>`Ticker`</th>`
`<th>`Price`</th>`
`<th>`Change`</th>`
`</tr>`
`</thead>`
`<tbody>`
{stocks &&
Object.entries(stocks).map(([ticker, data]) => (
`<tr key={ticker}>`
`<td>`{ticker}`</td>`
`<td>`${data.price?.toFixed(2)}`</td>`
`<td style={{ color: data.change >= 0 ? 'green' : 'red' }}>`
{data.change >= 0 ? '+' : ''}{data.change?.toFixed(2)}%
`</td>`
`</tr>`
))}
`</tbody>`
`</table>`
`</div>`
`</div>`
`</div>`
);
}
Server Implementation for Dashboard Streams
A Node.js server that broadcasts metrics, stocks, and activity:
import WebSocket from 'ws';
import http from 'http';
const server = http.createServer();
const wss = new WebSocket.Server({ server });
const subscriptions = {
metrics: new Set(),
stocks: new Set(),
activity: new Set(),
};
wss.on('connection', (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const stream = url.pathname.split('/').pop();
if (subscriptions[stream]) {
subscriptions[stream].add(ws);
}
ws.on('close', () => {
subscriptions.metrics.delete(ws);
subscriptions.stocks.delete(ws);
subscriptions.activity.delete(ws);
});
});
// Broadcast metrics every second
setInterval(() => {
const metric = {
type: 'metric',
timestamp: Date.now(),
cpu: Math.random() * 100,
memory: Math.random() * 100,
network: Math.random() * 1000,
};
subscriptions.metrics.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(metric));
}
});
}, 1000);
// Broadcast stock updates every 2 seconds
setInterval(() => {
const tickers = ['AAPL', 'MSFT', 'GOOGL'];
tickers.forEach((ticker) => {
const price = {
type: 'price',
ticker,
price: 100 + Math.random() * 50,
change: (Math.random() - 0.5) * 5,
timestamp: Date.now(),
};
subscriptions.stocks.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(price));
}
});
});
}, 2000);
// Broadcast activity
setInterval(() => {
const activity = {
type: 'activity',
event: ['user-join', 'user-leave', 'signup', 'transaction'][
Math.floor(Math.random() * 4)
],
};
subscriptions.activity.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(activity));
}
});
}, 500);
server.listen(3000);
Error Recovery for Streams
If one stream fails, don't abandon the entire dashboard. Implement fallback rendering:
export function DashboardWithFallback() {
const metricsQuery = useServerMetrics();
const stocksQuery = useStockPrices();
const activityQuery = useUserActivity();
return (
`<div>`
{metricsQuery.isError && (
`<div style={{ color: 'red' }}>`
Metrics stream error. Showing last known data.
`</div>`
)}
{metricsQuery.data ? (
`<MetricsChart data={metricsQuery.data} />`
) : (
`<div>`Loading metrics...`</div>`
)}
{stocksQuery.isError && (
`<div style={{ color: 'red' }}>`
Stock prices unavailable.
`</div>`
)}
{stocksQuery.data ? (
`<StockTable data={stocksQuery.data} />`
) : (
`<div>`Loading stocks...`</div>`
)}
`</div>`
);
}
Performance: Handling 1000+ Updates Per Second
For high-frequency dashboards, optimize rendering:
export function HighFrequencyDashboard() {
// Use batching (article 8) for metrics
const addMetricBatch = useBatchedMessages((batch) => {
queryClient.setQueryData(['metrics'], (old) =>
[
...old.slice(-59),
...batch.map((m) => ({
timestamp: m.timestamp,
value: m.value,
})),
]
);
});
// Use circular buffer (article 8) for unlimited streams
const metricsBufferRef = useRef(new CircularBuffer(1000));
useWebSocket('wss://your-server.example.com/stream', {
onMessage: (msg) => {
if (msg.type === 'metric') {
metricsBufferRef.current.add(msg);
addMetricBatch(msg);
}
},
});
// Render only every 100ms
const [, setTick] = useState(0);
useEffect(() => {
const interval = setInterval(() => setTick((t) => t + 1), 100);
return () => clearInterval(interval);
}, []);
return `<MetricsChart data={metricsBufferRef.current.getAll()} />`;
}
Key Takeaways
- A live dashboard manages multiple WebSocket streams independently.
- Use React Query with
setQueryData()to sync each stream's cache. - Batch high-frequency updates and implement fallback UI if a stream fails.
- Keep only the last N data points (60 for time-series, 1000 for history) to prevent memory bloat.
- Render charts efficiently: throttle renders to 10–30 fps, not per-message.
Frequently Asked Questions
How do I handle subscriptions to 50+ metrics?
Group related metrics into streams. Instead of 50 connections, use 5 streams (metrics, logs, user-events, system-health, transactions). Or use a single WebSocket with channel routing.
Should I use Redux or Zustand instead of React Query?
React Query is better because it's built for server state, handles caching and sync automatically, and provides tools like devtools and offline support. Redux is for app state (UI, user preferences).
How do I export dashboard data as a CSV?
Store data in React Query cache or a ref, then generate CSV on click: const csv = Papa.unparse(data); downloadAsFile(csv, 'dashboard.csv').
Can I render 1000 data points in a chart?
Yes, but use a library like Recharts with virtual scrolling or reduce the data (every 10th point). For very large datasets, use Canvas-based libraries like Vega or Deck.gl.