Skip to content

Mini Project: Live Dashboard with SSE

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Mini Project: Live Dashboard with SSE. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a real-time live dashboard using SSE: create server-sent events for metrics streaming, build a browser dashboard with EventSource, and implement production features like reconnection and event history.

What You Learn

You will build a complete live monitoring dashboard using SSE. The project covers SSE server implementation, browser-side EventSource client, multiple event types, auto-reconnection, event history, and a clean dashboard UI.

Why It Matters

A real-time dashboard is one of the most practical SSE applications. Building it from scratch teaches you the full SSE stack: server events, client handling, reconnection logic, and browser rendering. This project mirrors real DodaTech monitoring tools.

Real-World Use

DodaTech's internal infrastructure dashboard uses SSE to push server metrics, deployment status, and alert notifications to operations teams. The same pattern powers live analytics dashboards, stock tickers, and sports scoreboards.

Project Overview

graph LR
    Server[Node.js SSE Server] -->|event stream| Browser1[Browser Tab 1]
    Server -->|event stream| Browser2[Browser Tab 2]
    Server -->|event stream| Browser3[Browser Tab 3]
    Server -->|generates| CPU[CPU Metrics]
    Server -->|generates| Memory[Memory Metrics]
    Server -->|generates| Network[Network Metrics]
    Server -->|generates| Alerts[Alert Events]

The server generates simulated system metrics and pushes them to all connected browsers via SSE. The browser dashboard displays real-time charts, status indicators, and alert notifications.

Server Implementation

const http = require('http');

class DashboardSSEServer {
    constructor(port = 3000) {
        this.port = port;
        this.clients = new Map();
        this.clientId = 0;
        this.eventHistory = [];
        this.maxHistory = 200;
        this.metrics = {
            cpu: 45,
            memory: 62,
            networkIn: 100,
            networkOut: 80,
            uptime: 0,
            requestsPerSec: 150,
        };
    }

    start() {
        const server = http.createServer((req, res) => {
            if (req.url === '/dashboard/stream') {
                this.handleSSE(req, res);
            } else if (req.url === '/dashboard/history') {
                this.handleHistory(req, res);
            } else {
                this.serveDashboard(req, res);
            }
        });

        this.startMetricsSimulation();

        server.listen(this.port, () => {
            console.log(`Dashboard on http://localhost:${this.port}`);
        });
    }

    handleSSE(req, res) {
        this.clientId++;
        const clientId = this.clientId;

        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Access-Control-Allow-Origin': '*',
        });

        // Send retry interval
        res.write('retry: 3000\n\n');

        // Send initial connection event
        this.sendEvent(res, 'connected', {
            clientId,
            message: 'Dashboard connected',
            serverTime: Date.now(),
        });

        this.clients.set(clientId, res);
        console.log(`Client ${clientId} connected (${this.clients.size} total)`);

        req.on('close', () => {
            this.clients.delete(clientId);
            console.log(`Client ${clientId} disconnected`);
        });
    }

    sendEvent(res, event, data) {
        try {
            res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
            return true;
        } catch (e) {
            return false;
        }
    }

    broadcast(event, data) {
        const entry = { event, data, timestamp: Date.now() };
        this.eventHistory.push(entry);
        if (this.eventHistory.length > this.maxHistory) {
            this.eventHistory.shift();
        }

        for (const [id, client] of this.clients) {
            if (!this.sendEvent(client, event, data)) {
                this.clients.delete(id);
            }
        }
    }

    handleHistory(req, res) {
        res.writeHead(200, {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
        });
        res.end(JSON.stringify(this.eventHistory.slice(-50)));
    }

    serveDashboard(req, res) {
        const html = this.getDashboardHTML();
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(html);
    }

    startMetricsSimulation() {
        // Update metrics every 2 seconds
        setInterval(() => {
            this.metrics.cpu = Math.max(0, Math.min(100,
                this.metrics.cpu + (Math.random() - 0.5) * 10
            ));
            this.metrics.memory = Math.max(30, Math.min(95,
                this.metrics.memory + (Math.random() - 0.5) * 5
            ));
            this.metrics.networkIn = Math.max(0,
                this.metrics.networkIn + (Math.random() - 0.5) * 20
            );
            this.metrics.networkOut = Math.max(0,
                this.metrics.networkOut + (Math.random() - 0.5) * 15
            );
            this.metrics.uptime += 2;
            this.metrics.requestsPerSec = Math.max(50, Math.min(500,
                this.metrics.requestsPerSec + (Math.random() - 0.5) * 30
            ));

            this.broadcast('metrics', { ...this.metrics });

            // Random alerts
            if (this.metrics.cpu > 85) {
                this.broadcast('alert', {
                    type: 'warning',
                    message: `High CPU usage: ${this.metrics.cpu.toFixed(1)}%`,
                    time: new Date().toISOString(),
                });
            }
            if (this.metrics.memory > 90) {
                this.broadcast('alert', {
                    type: 'critical',
                    message: `Critical memory: ${this.metrics.memory.toFixed(1)}%`,
                    time: new Date().toISOString(),
                });
            }
        }, 2000);
    }

    getDashboardHTML() {
        return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; padding: 20px; }
h1 { font-size: 1.5rem; margin-bottom: 20px; color: #38bdf8; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: #1e293b; border-radius: 8px; padding: 16px; border: 1px solid #334155; }
.stat-label { font-size: 0.75rem; text-transform: uppercase; color: #94a3b8; margin-bottom: 4px; }
.stat-value { font-size: 1.5rem; font-weight: 700; }
.stat-unit { font-size: 0.75rem; color: #64748b; }
.bar-container { background: #334155; border-radius: 4px; height: 8px; margin-top: 8px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
.alerts { background: #1e293b; border-radius: 8px; padding: 16px; border: 1px solid #334155; }
.alerts h2 { font-size: 1rem; margin-bottom: 12px; }
.alert-item { padding: 8px 12px; margin-bottom: 4px; border-radius: 4px; font-size: 0.875rem; }
.alert-item.warning { background: #451a03; border-left: 3px solid #f59e0b; }
.alert-item.critical { background: #450a0a; border-left: 3px solid #ef4444; }
.alert-time { font-size: 0.75rem; color: #64748b; }
.connection-status { position: fixed; top: 20px; right: 20px; padding: 8px 16px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.connected { background: #065f46; color: #6ee7b7; }
.disconnected { background: #7f1d1d; color: #fca5a5; }
.reconnecting { background: #713f12; color: #fcd34d; }
</style>
</head>
<body>
<h1>System Dashboard</h1>

<div id="connectionStatus" class="connection-status disconnected">Disconnected</div>

<div id="statsGrid" class="stats-grid">
<div class="stat-card">
<div class="stat-label">CPU Usage</div>
<div id="cpuValue" class="stat-value">--</div>
<div class="bar-container"><div id="cpuBar" class="bar-fill" style="width:0%;background:#38bdf8"></div></div>
</div>
<div class="stat-card">
<div class="stat-label">Memory</div>
<div id="memValue" class="stat-value">--</div>
<div class="bar-container"><div id="memBar" class="bar-fill" style="width:0%;background:#a78bfa"></div></div>
</div>
<div class="stat-card">
<div class="stat-label">Network In</div>
<div id="netInValue" class="stat-value">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Network Out</div>
<div id="netOutValue" class="stat-value">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Requests/sec</div>
<div id="reqValue" class="stat-value">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Uptime</div>
<div id="uptimeValue" class="stat-value">--</div>
</div>
</div>

<div id="alertsSection" class="alerts">
<h2>Alerts</h2>
<div id="alertList"></div>
</div>

<script>
class DashboardClient {
constructor() {
this.eventSource = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.connect();
}

connect() {
this.eventSource = new EventSource('/dashboard/stream');
this.updateStatus('connecting', 'Connecting...');

this.eventSource.onopen = () => {
this.updateStatus('connected', 'Connected');
this.reconnectAttempts = 0;
};

this.eventSource.onerror = () => {
this.eventSource.close();
this.updateStatus('disconnected', 'Disconnected');
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.updateStatus('reconnecting', 'Reconnecting in ' + (delay/1000) + 's...');
setTimeout(() => this.connect(), delay);
}
};

this.eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
console.log('Connected:', data);
});

this.eventSource.addEventListener('metrics', (e) => {
const data = JSON.parse(e.data);
this.updateMetrics(data);
});

this.eventSource.addEventListener('alert', (e) => {
const data = JSON.parse(e.data);
this.addAlert(data);
});

this.eventSource.addEventListener('shutdown', (e) => {
const data = JSON.parse(e.data);
this.eventSource.close();
setTimeout(() => this.connect(), data.reconnectDelay || 5000);
});
}

updateStatus(status, text) {
const el = document.getElementById('connectionStatus');
el.className = 'connection-status ' + status;
el.textContent = text;
}

updateMetrics(m) {
document.getElementById('cpuValue').textContent = m.cpu.toFixed(1) + '%';
document.getElementById('cpuBar').style.width = m.cpu + '%';
document.getElementById('memValue').textContent = m.memory.toFixed(1) + '%';
document.getElementById('memBar').style.width = m.memory + '%';
document.getElementById('netInValue').textContent = m.networkIn.toFixed(0) + ' Mbps';
document.getElementById('netOutValue').textContent = m.networkOut.toFixed(0) + ' Mbps';
document.getElementById('reqValue').textContent = m.requestsPerSec.toFixed(0);
document.getElementById('uptimeValue').textContent = m.uptime + 's';
}

addAlert(alert) {
const list = document.getElementById('alertList');
const item = document.createElement('div');
item.className = 'alert-item ' + (alert.type || 'info');
item.innerHTML = '<div>' + alert.message + '</div><div class="alert-time">' + alert.time + '</div>';
list.prepend(item);

// Keep max 20 alerts
while (list.children.length > 20) {
list.removeChild(list.lastChild);
}
}
}

new DashboardClient();
</script>
</body>
</html>`;
    }
}

const dashboard = new DashboardSSEServer();
dashboard.start();

Expected output: Server starts on port 3000, serves the dashboard HTML at http://localhost:3000, streams metrics every 2 seconds, sends alert events for high CPU/memory, and clients auto-reconnect on disconnection.

Running the Dashboard

node dashboard-server.js

Open http://localhost:3000 in your browser. You see 6 metric cards updating in real time. CPU and memory have progress bars. Alerts appear when thresholds are exceeded. The connection indicator shows green when connected.

Expected output:

  • CPU: fluctuates between 30-70%
  • Memory: fluctuates between 40-85%
  • Network In: 80-120 Mbps
  • Network Out: 60-100 Mbps
  • Requests/sec: 100-400
  • Alert: "High CPU usage: 87.3%" when CPU exceeds 85%

Testing Reconnection

// Kill the server while the dashboard is open
// Then restart it

// Server restart
dashboard.start();

// Browser automatically reconnects within 3 seconds
// The retry: 3000 directive in the SSE stream sets this
// No data is lost after reconnection (history endpoint)

Expected behavior: When the server stops, the browser shows "Disconnected" in red. When the server restarts, it reconnects within 3 seconds. The connection status turns green. Alerts and metrics resume.

Extending the Dashboard

// Add more metric sources
this.broadcast('disk', {
    total: 500,     // GB
    used: 320,      // GB
    free: 180,      // GB
    usagePercent: 64,
});

// Add custom event tracking
this.broadcast('userAction', {
    userId: 'user_' + Math.floor(Math.random() * 1000),
    action: 'page_view',
    page: '/dashboard',
    timestamp: Date.now(),
});

// Add historical data points for charting
const historyEndpoint = '/dashboard/history';
// Serves last 100 metric snapshots for chart rendering

Expected output: Extended dashboard shows disk usage in a third progress bar and user actions as a live feed. The history endpoint provides data for chart libraries like Chart.js.

Common Mistakes

1. Not Handling Server Restart

When the server restarts, all EventSource connections fail. Implement auto-reconnection with exponential backoff. Send a shutdown event before the server stops to prepare clients.

2. No Event History

Without event history, newly connected clients see empty dashboards. Store recent events in memory and replay them on connection. Provide a /history endpoint for initial data load.

3. Missing Connection Status

Users think the dashboard is broken when SSE disconnects. Show connection status prominently. Indicate reconnection attempts. This prevents confusion during network glitches.

4. Too Many Events

Sending events every 100ms overwhelms the browser render loop. Batch updates to 500ms-2s intervals. Use requestAnimationFrame on the client for smooth rendering of the batched data.

5. No Error Boundaries

If the server crashes, the dashboard shows stale data. Detect stale connections with heartbeat events. Show "Last updated X seconds ago" on each metric card. This signals freshness.

Practice Questions

1. How does the dashboard handle server restarts?

The EventSource fires onerror, the client closes the connection, waits 3 seconds (retry directive), and reconnects. Exponential backoff prevents reconnection storms.

2. Why is event history important in a dashboard?

New clients need initial data to fill the dashboard. Without history, they wait for the next event. The /history endpoint provides the last 50 events on connection.

3. How do you prevent browser blocking during SSE?

SSE batches events at 2-second intervals. Each event batch is small (one metrics object). The browser render loop updates the DOM once per animation frame, not per event.

4. What happens when the server exceeds memory limits?

The server caps event history at 200 entries. Old entries are shifted off. Client connections are tracked in a Map and cleaned up on close. This prevents unbounded memory growth.

Challenge

Extend the dashboard with: Chart.js line chart showing CPU and memory over time, disk usage metrics with progress bar, user action feed from server, /history endpoint seeded with 50 data points on first connection, multiple dashboard pages (system, network, users), and dark/light theme toggle.

FAQ

Can I use this dashboard for production monitoring?

Yes, extend it with auth, persistent storage, and alerting. Add Prometheus integration, Grafana-style dashboards, and notification channels like Slack or email.

Does this work with React or Vue?

Yes. Replace the vanilla JS client with React's useEffect and EventSource. The SSE protocol is framework-agnostic. The server code stays identical.

How do I add authentication to SSE endpoints?

Use cookies or tokens. SSE with EventSource cannot set custom headers. Embed an auth token in the URL: /dashboard/stream?token=abc123. Validate on the server.

Can I deploy this on a Raspberry Pi?

Yes. Node.js runs on ARM. The dashboard uses minimal resources: ~50MB RAM, ~5% CPU for 100 clients. Perfect for a home server monitoring setup.

How do I persist metrics across restarts?

Write metrics to a SQLite database. On server start, load the last 1000 data points. Serve them via /history. Append new points in real time.

Mini Project: Multi-Page Dashboard

Build a multi-page monitoring dashboard with SSE. Include: system overview page (CPU, memory, disk), network page (in/out traffic, active connections), logs page (streaming log events), alert history page (persisted alerts), settings page (configurable thresholds), and a navigation menu that keeps SSE connected across page switches.

What's Next

Now that you have built a complete SSE dashboard, apply these skills to Webhooks for server-to-server real-time communication, or explore Message Queue Patterns for event-driven architectures.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro