SysTutorials

Real-Time System Resource Monitoring with WebSockets

Java Applets were obsolete by 2016 and removed from Java 9+. If you need a browser-based system monitoring dashboard, use WebSockets or Server-Sent Events paired with modern JavaScript charting libraries.

WebSocket + Python Backend Architecture

The most practical setup streams metrics from a Python backend to connected clients using WebSockets. Clients render real-time charts without server-side templating.

Backend Metrics Collector

#!/usr/bin/env python3
import psutil
import json
import asyncio
import os
from websockets.server import serve
from datetime import datetime

async def broadcast_metrics(websocket, path):
    try:
        while True:
            metrics = {
                'cpu': psutil.cpu_percent(interval=1),
                'memory': psutil.virtual_memory().percent,
                'disk': psutil.disk_usage('/').percent,
                'load_avg': list(os.getloadavg()),
                'timestamp': datetime.utcnow().isoformat()
            }
            await websocket.send(json.dumps(metrics))
            await asyncio.sleep(2)
    except asyncio.CancelledError:
        pass

async def main():
    async with serve(broadcast_metrics, '0.0.0.0', 8080):
        print("Metrics server running on ws://0.0.0.0:8080")
        await asyncio.Future()

if __name__ == '__main__':
    asyncio.run(main())

For production, wrap with TLS and add authentication:

import ssl
from functools import wraps

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain('cert.pem', 'key.pem')

async def authenticate(websocket, path):
    token = websocket.request_headers.get('Authorization', '').replace('Bearer ', '')
    if not validate_token(token):
        await websocket.close(code=1008, reason='Unauthorized')
        return
    await broadcast_metrics(websocket, path)

async def main():
    async with serve(authenticate, '0.0.0.0', 8080, ssl=ssl_context):
        print("Secure metrics server running on wss://0.0.0.0:8080")
        await asyncio.Future()

Frontend Client

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>System Monitor</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
        .container { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding: 20px; }
        .card { border: 1px solid #ddd; border-radius: 8px; padding: 15px; }
        canvas { max-height: 300px; }
        .status { padding: 10px; border-radius: 4px; font-size: 14px; }
        .status.connected { background: #d4edda; color: #155724; }
        .status.disconnected { background: #f8d7da; color: #721c24; }
    </style>
</head>
<body>
    <div class="status disconnected" id="status">Disconnected</div>
    <div class="container">
        <div class="card"><canvas id="cpuChart"></canvas></div>
        <div class="card"><canvas id="memoryChart"></canvas></div>
        <div class="card"><canvas id="diskChart"></canvas></div>
        <div class="card"><canvas id="loadChart"></canvas></div>
    </div>

    <script>
        const maxDataPoints = 60;
        const metrics = { 
            labels: [], 
            cpu: [], 
            memory: [], 
            disk: [],
            load_avg: []
        };

        const chartConfigs = {
            cpu: {
                label: 'CPU %',
                color: 'rgb(255, 99, 132)',
                bgColor: 'rgba(255, 99, 132, 0.1)'
            },
            memory: {
                label: 'Memory %',
                color: 'rgb(75, 192, 192)',
                bgColor: 'rgba(75, 192, 192, 0.1)'
            },
            disk: {
                label: 'Disk %',
                color: 'rgb(255, 193, 7)',
                bgColor: 'rgba(255, 193, 7, 0.1)'
            },
            load_avg: {
                label: 'Load Average',
                color: 'rgb(153, 102, 255)',
                bgColor: 'rgba(153, 102, 255, 0.1)'
            }
        };

        function createChart(canvasId, metric) {
            return new Chart(document.getElementById(canvasId), {
                type: 'line',
                data: {
                    labels: metrics.labels,
                    datasets: [{
                        label: chartConfigs[metric].label,
                        data: metrics[metric],
                        borderColor: chartConfigs[metric].color,
                        backgroundColor: chartConfigs[metric].bgColor,
                        tension: 0.3,
                        fill: true,
                        pointRadius: 0
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: true,
                    scales: {
                        y: { 
                            max: 100, 
                            min: 0,
                            ticks: { callback: v => v + '%' }
                        }
                    },
                    plugins: {
                        legend: { display: true }
                    }
                }
            });
        }

        const charts = {
            cpu: createChart('cpuChart', 'cpu'),
            memory: createChart('memoryChart', 'memory'),
            disk: createChart('diskChart', 'disk'),
            load_avg: createChart('loadChart', 'load_avg')
        };

        function connectWebSocket() {
            const token = localStorage.getItem('auth_token');
            const socket = new WebSocket('wss://monitoring-server:8080');
            let reconnectAttempts = 0;
            const maxAttempts = 10;

            socket.onopen = () => {
                if (token) {
                    socket.send(JSON.stringify({ type: 'auth', token }));
                }
                updateStatus(true);
                reconnectAttempts = 0;
            };

            socket.onmessage = (event) => {
                const data = JSON.parse(event.data);
                const time = new Date(data.timestamp).toLocaleTimeString();

                metrics.labels.push(time);
                metrics.cpu.push(data.cpu);
                metrics.memory.push(data.memory);
                metrics.disk.push(data.disk);
                metrics.load_avg.push(data.load_avg[0]); // 1-minute load average

                if (metrics.labels.length > maxDataPoints) {
                    metrics.labels.shift();
                    metrics.cpu.shift();
                    metrics.memory.shift();
                    metrics.disk.shift();
                    metrics.load_avg.shift();
                }

                Object.values(charts).forEach(chart => chart.update('none'));
            };

            socket.onerror = () => {
                console.error('WebSocket error');
                updateStatus(false);
            };

            socket.onclose = () => {
                updateStatus(false);
                if (reconnectAttempts < maxAttempts) {
                    const delay = Math.pow(2, reconnectAttempts) * 1000;
                    reconnectAttempts++;
                    console.log(`Reconnecting in ${delay}ms`);
                    setTimeout(connectWebSocket, delay);
                }
            };

            return socket;
        }

        function updateStatus(connected) {
            const el = document.getElementById('status');
            el.textContent = connected ? 'Connected' : 'Disconnected';
            el.className = 'status ' + (connected ? 'connected' : 'disconnected');
        }

        connectWebSocket();
    </script>
</body>
</html>

WebSockets vs Server-Sent Events

WebSockets provide bidirectional communication—useful when clients request specific metrics or adjust collection intervals. Server-Sent Events (SSE) work for unidirectional streams and use simpler HTTP, with better load-balancer compatibility.

Use SSE if you only need one-way data flow:

from aiohttp import web

async def metrics_stream(request):
    response = web.StreamResponse()
    response.headers['Content-Type'] = 'text/event-stream'
    response.headers['Cache-Control'] = 'no-cache'
    response.headers['Connection'] = 'keep-alive'
    await response.prepare(request)

    try:
        while True:
            metrics = {
                'cpu': psutil.cpu_percent(interval=1),
                'memory': psutil.virtual_memory().percent,
                'timestamp': datetime.utcnow().isoformat()
            }
            await response.write(f"data: {json.dumps(metrics)}\n\n".encode())
            await asyncio.sleep(2)
    except asyncio.CancelledError:
        pass

    return response

Client-side SSE:

const eventSource = new EventSource('https://monitoring-server/metrics');

eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    updateCharts(data);
};

eventSource.onerror = () => {
    console.error('SSE connection lost');
    eventSource.close();
    setTimeout(() => location.reload(), 5000);
};

Security Requirements

Collection Intervals and Performance

Handling Disconnections and Reconnection

function connectWebSocket() {
    const socket = new WebSocket('wss://monitoring-server:8080');
    let reconnectAttempts = 0;
    const maxAttempts = 10;

    socket.onopen = () => {
        console.log('Connected');
        reconnectAttempts = 0;
    };

    socket.onclose = () => {
        if (reconnectAttempts < maxAttempts) {
            const delay = Math.pow(2, reconnectAttempts) * 1000;
            console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts + 1})`);
            reconnectAttempts++;
            setTimeout(connectWebSocket, delay);
        } else {
            console.error('Max reconnection attempts reached');
        }
    };

    socket.onerror = (error) => {
        console.error('WebSocket error:', error);
    };

    return socket;
}

Production Deployments

For anything beyond testing:

Prometheus + Grafana: Install the Prometheus node exporter on target systems, scrape metrics into Prometheus time-series database, visualize in Grafana. Excellent for multi-host monitoring with built-in alerting.

Telegraf + InfluxDB: High-performance metrics pipeline. Telegraf collects from diverse sources (HTTP, databases, system), InfluxDB stores, Grafana visualizes. Better for large cardinality datasets.

Zabbix: Open-source with distributed agents, trend analysis, sophisticated alerting, and compliance reporting. Suitable for enterprise deployments needing SNMP integration and custom triggers.

Managed SaaS: Datadog, New Relic, Honeycomb, or Elastic Cloud. Pay per host/metric volume; offloads infrastructure concerns.

Monitoring Your Monitoring

Track these metrics on your monitoring system:

Set alerts on unexpected disconnects or high reconnection rates—they indicate network issues or backend problems.

Advantages Over Legacy Approaches

Java Applets required browser plugins, failed silently on permission denial, incurred JVM startup overhead, and introduced attack surface through outdated security models. Modern web stacks run natively in browsers, deploy instantly without installation, use standardized TLS security and CORS headers, and scale to hundreds of concurrent clients efficiently. Development is simpler—no need for Java expertise alongside web development.

← All posts