Proxy Health Monitor: Build Your Own with Node.js

Proxy Health Monitor: Build Your Own with Node.js

Proxy health monitoring is essential when you manage a pool of proxies. A dedicated monitor continuously tests each proxy, tracks latency trends, detects outages, and sends alerts when proxies fail. This tutorial builds a complete monitoring service in Node.js with a REST API and real-time WebSocket updates.

Features

  • Continuous health checking with configurable intervals
  • Latency tracking and historical trends
  • WebSocket-based real-time status updates
  • REST API for querying proxy status
  • Email/webhook alerting on failures
  • SQLite persistence for check history
  • Simple web dashboard

Project Setup

mkdir proxy-monitor && cd proxy-monitor
npm init -y
npm install express ws better-sqlite3 node-fetch https-proxy-agent socks-proxy-agent node-cron

Core Monitor

// monitor.js
const fetch = require('node-fetch');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');

class ProxyMonitor {
    constructor(options = {}) {
        this.testUrl = options.testUrl || 'https://httpbin.org/ip';
        this.timeout = options.timeout || 10000;
        this.checkInterval = options.checkInterval || 60000;
        this.maxHistory = options.maxHistory || 1000;
        this.proxies = new Map();
        this.listeners = [];
        this._timers = new Map();
    }

    addProxy(id, url, metadata = {}) {
        this.proxies.set(id, {
            id,
            url,
            metadata,
            status: 'unknown',
            alive: false,
            latencyMs: 0,
            lastCheck: null,
            lastAlive: null,
            consecutiveFailures: 0,
            totalChecks: 0,
            totalFailures: 0,
            history: [],
            externalIp: null,
        });
    }

    removeProxy(id) {
        this.proxies.delete(id);
        const timer = this._timers.get(id);
        if (timer) {
            clearInterval(timer);
            this._timers.delete(id);
        }
    }

    async checkProxy(id) {
        const proxy = this.proxies.get(id);
        if (!proxy) return null;

        const start = Date.now();
        let result = {
            timestamp: new Date().toISOString(),
            alive: false,
            latencyMs: 0,
            statusCode: 0,
            error: null,
            externalIp: null,
        };

        try {
            const agent = this._createAgent(proxy.url);
            const controller = new AbortController();
            const timeoutId = setTimeout(
                () => controller.abort(), this.timeout
            );

            const response = await fetch(this.testUrl, {
                agent,
                signal: controller.signal,
                headers: { 'User-Agent': 'ProxyMonitor/1.0' },
            });
            clearTimeout(timeoutId);

            result.latencyMs = Date.now() - start;
            result.statusCode = response.status;

            if (response.ok) {
                const data = await response.json();
                result.alive = true;
                result.externalIp = data.origin || null;
            }
        } catch (err) {
            result.latencyMs = Date.now() - start;
            result.error = err.message;
        }

        // Update proxy state
        proxy.status = result.alive ? 'healthy' : 'down';
        proxy.alive = result.alive;
        proxy.latencyMs = result.latencyMs;
        proxy.lastCheck = result.timestamp;
        proxy.totalChecks++;
        proxy.externalIp = result.externalIp || proxy.externalIp;

        if (result.alive) {
            const wasDown = proxy.consecutiveFailures > 0;
            proxy.consecutiveFailures = 0;
            proxy.lastAlive = result.timestamp;
            if (wasDown) {
                this._emit('recovery', { id, proxy: proxy });
            }
        } else {
            proxy.consecutiveFailures++;
            proxy.totalFailures++;
            if (proxy.consecutiveFailures === 3) {
                this._emit('alert', {
                    id,
                    message: `Proxy ${id} failed 3 consecutive checks`,
                    proxy,
                });
            }
        }

        // Maintain history
        proxy.history.push(result);
        if (proxy.history.length > this.maxHistory) {
            proxy.history.shift();
        }

        this._emit('check', { id, result, proxy });
        return result;
    }

    async checkAll() {
        const promises = Array.from(this.proxies.keys()).map(
            id => this.checkProxy(id)
        );
        return Promise.allSettled(promises);
    }

    startMonitoring() {
        // Initial check
        this.checkAll();

        // Periodic checks
        for (const id of this.proxies.keys()) {
            const timer = setInterval(
                () => this.checkProxy(id),
                this.checkInterval
            );
            this._timers.set(id, timer);
        }
    }

    stopMonitoring() {
        for (const timer of this._timers.values()) {
            clearInterval(timer);
        }
        this._timers.clear();
    }

    onEvent(listener) {
        this.listeners.push(listener);
    }

    _emit(event, data) {
        for (const listener of this.listeners) {
            try {
                listener(event, data);
            } catch (err) {
                console.error('Listener error:', err);
            }
        }
    }

    _createAgent(proxyUrl) {
        if (proxyUrl.startsWith('socks')) {
            return new SocksProxyAgent(proxyUrl);
        }
        return new HttpsProxyAgent(proxyUrl);
    }

    getStatus() {
        const proxies = Array.from(this.proxies.values());
        return {
            total: proxies.length,
            alive: proxies.filter(p => p.alive).length,
            down: proxies.filter(p => !p.alive && p.status !== 'unknown').length,
            unknown: proxies.filter(p => p.status === 'unknown').length,
            avgLatency: proxies.filter(p => p.alive).length > 0
                ? Math.round(
                    proxies.filter(p => p.alive)
                        .reduce((sum, p) => sum + p.latencyMs, 0)
                    / proxies.filter(p => p.alive).length
                  )
                : 0,
        };
    }
}

module.exports = ProxyMonitor;

REST API and WebSocket Server

// server.js
const express = require('express');
const { WebSocketServer } = require('ws');
const http = require('http');
const ProxyMonitor = require('./monitor');

const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });

app.use(express.json());

// Initialize monitor
const monitor = new ProxyMonitor({
    checkInterval: 30000,
    timeout: 10000,
});

// Load proxies from config or request
const defaultProxies = [
    { id: 'proxy-1', url: 'http://proxy1.example.com:8080' },
    { id: 'proxy-2', url: 'http://proxy2.example.com:8080' },
];
defaultProxies.forEach(p => monitor.addProxy(p.id, p.url));

// WebSocket broadcast
monitor.onEvent((event, data) => {
    const message = JSON.stringify({
        event,
        data: {
            id: data.id,
            status: data.proxy?.status,
            alive: data.proxy?.alive,
            latencyMs: data.proxy?.latencyMs,
            lastCheck: data.proxy?.lastCheck,
            message: data.message,
        },
    });

    wss.clients.forEach(client => {
        if (client.readyState === 1) {
            client.send(message);
        }
    });
});

// REST endpoints
app.get('/api/status', (req, res) => {
    res.json(monitor.getStatus());
});

app.get('/api/proxies', (req, res) => {
    const proxies = Array.from(monitor.proxies.values()).map(p => ({
        id: p.id,
        url: p.url,
        status: p.status,
        alive: p.alive,
        latencyMs: p.latencyMs,
        lastCheck: p.lastCheck,
        consecutiveFailures: p.consecutiveFailures,
        uptimePercent: p.totalChecks > 0
            ? ((1 - p.totalFailures / p.totalChecks) * 100).toFixed(1)
            : 'N/A',
    }));
    res.json(proxies);
});

app.get('/api/proxies/:id', (req, res) => {
    const proxy = monitor.proxies.get(req.params.id);
    if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
    res.json(proxy);
});

app.get('/api/proxies/:id/history', (req, res) => {
    const proxy = monitor.proxies.get(req.params.id);
    if (!proxy) return res.status(404).json({ error: 'Proxy not found' });
    const limit = parseInt(req.query.limit) || 100;
    res.json(proxy.history.slice(-limit));
});

app.post('/api/proxies', (req, res) => {
    const { id, url, metadata } = req.body;
    if (!id || !url) {
        return res.status(400).json({ error: 'id and url required' });
    }
    monitor.addProxy(id, url, metadata);
    monitor.checkProxy(id);
    res.status(201).json({ message: 'Proxy added', id });
});

app.delete('/api/proxies/:id', (req, res) => {
    monitor.removeProxy(req.params.id);
    res.json({ message: 'Proxy removed' });
});

app.post('/api/check', async (req, res) => {
    const results = await monitor.checkAll();
    res.json(monitor.getStatus());
});

app.post('/api/check/:id', async (req, res) => {
    const result = await monitor.checkProxy(req.params.id);
    if (!result) return res.status(404).json({ error: 'Proxy not found' });
    res.json(result);
});

// Simple dashboard
app.get('/', (req, res) => {
    res.send(`
    <!DOCTYPE html>
    <html>
    <head>
        <title>Proxy Health Monitor</title>
        <style>
            body { font-family: monospace; background: #1a1a2e; color: #eee; padding: 20px; }
            .proxy { padding: 10px; margin: 5px 0; border-radius: 4px; }
            .healthy { background: #16213e; border-left: 4px solid #0f0; }
            .down { background: #16213e; border-left: 4px solid #f00; }
            .unknown { background: #16213e; border-left: 4px solid #888; }
            h1 { color: #e94560; }
            #status { font-size: 1.2em; margin: 10px 0; }
        </style>
    </head>
    <body>
        <h1>Proxy Health Monitor</h1>
        <div id="status"></div>
        <div id="proxies"></div>
        <script>
            const ws = new WebSocket('ws://' + location.host);
            ws.onmessage = (e) => { fetchProxies(); };
            async function fetchProxies() {
                const [statusRes, proxiesRes] = await Promise.all([
                    fetch('/api/status').then(r => r.json()),
                    fetch('/api/proxies').then(r => r.json()),
                ]);
                document.getElementById('status').innerHTML =
                    'Alive: ' + statusRes.alive + '/' + statusRes.total +
                    ' | Avg Latency: ' + statusRes.avgLatency + 'ms';
                document.getElementById('proxies').innerHTML = proxiesRes.map(p =>
                    '<div class="proxy ' + p.status + '">' +
                    '<b>' + p.id + '</b> — ' + p.status.toUpperCase() +
                    ' | ' + p.latencyMs + 'ms | Uptime: ' + p.uptimePercent + '%' +
                    '</div>'
                ).join('');
            }
            fetchProxies();
            setInterval(fetchProxies, 10000);
        </script>
    </body>
    </html>
    `);
});

// Start
monitor.startMonitoring();
server.listen(3001, () => {
    console.log('Proxy Monitor running on http://localhost:3001');
});

SQLite Persistence

Store check history in SQLite for long-term analysis:

// db.js
const Database = require('better-sqlite3');
const db = new Database('proxy_monitor.db');

db.exec(`
    CREATE TABLE IF NOT EXISTS check_results (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        proxy_id TEXT NOT NULL,
        timestamp TEXT NOT NULL,
        alive INTEGER NOT NULL,
        latency_ms REAL,
        status_code INTEGER,
        error TEXT,
        external_ip TEXT
    );
    CREATE INDEX IF NOT EXISTS idx_proxy_timestamp
        ON check_results(proxy_id, timestamp);
`);

const insertCheck = db.prepare(`
    INSERT INTO check_results
    (proxy_id, timestamp, alive, latency_ms, status_code, error, external_ip)
    VALUES (?, ?, ?, ?, ?, ?, ?)
`);

const getHistory = db.prepare(`
    SELECT * FROM check_results
    WHERE proxy_id = ?
    ORDER BY timestamp DESC
    LIMIT ?
`);

const getUptimeStats = db.prepare(`
    SELECT
        proxy_id,
        COUNT(*) as total_checks,
        SUM(alive) as alive_checks,
        ROUND(AVG(CASE WHEN alive THEN latency_ms END), 0) as avg_latency,
        ROUND(CAST(SUM(alive) AS FLOAT) / COUNT(*) * 100, 1) as uptime_pct
    FROM check_results
    WHERE proxy_id = ?
    AND timestamp > datetime('now', ?)
    GROUP BY proxy_id
`);

module.exports = { db, insertCheck, getHistory, getUptimeStats };

Webhook Alerting

Send alerts to Slack, Discord, or any webhook endpoint:

// alerting.js
const fetch = require('node-fetch');

class AlertManager {
    constructor(options = {}) {
        this.webhookUrl = options.webhookUrl;
        this.cooldownMs = options.cooldownMs || 300000; // 5 min cooldown
        this._lastAlert = new Map();
    }

    async sendAlert(proxyId, message, severity = 'warning') {
        const now = Date.now();
        const lastSent = this._lastAlert.get(proxyId) || 0;

        if (now - lastSent < this.cooldownMs) return;

        this._lastAlert.set(proxyId, now);

        if (this.webhookUrl) {
            try {
                await fetch(this.webhookUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        text: `[${severity.toUpperCase()}] ${message}`,
                        proxy_id: proxyId,
                        timestamp: new Date().toISOString(),
                    }),
                });
            } catch (err) {
                console.error('Alert delivery failed:', err.message);
            }
        }

        console.log(`[ALERT] ${message}`);
    }
}

module.exports = AlertManager;

Deployment

Deploy the monitor using Docker:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]

Run with Docker:

docker build -t proxy-monitor .
docker run -d -p 3001:3001 -v $(pwd)/data:/app/data proxy-monitor

Internal Links

FAQ

How often should I check proxy health?

For paid proxies, check every 60 seconds. For free proxies, check every 15-30 seconds since they fail more frequently. Adjust based on your proxy count — checking 1,000 proxies every 15 seconds generates significant traffic.

Should I run the monitor on the same server as my scraper?

No. Run the monitor on a separate machine or container so it can detect network-level issues that affect your scraper. If the scraper machine has connectivity problems, the monitor should still be able to report the issue.

How do I monitor SOCKS proxies?

The monitor supports SOCKS4 and SOCKS5 proxies through the socks-proxy-agent library. Add them with the socks5://host:port URL format. The health check works identically — it tests connectivity and measures latency through the SOCKS tunnel.

What should my alert thresholds be?

Alert after 3 consecutive failures (about 90 seconds with 30-second checks). This avoids false alarms from single timeouts. For critical scrapers, alert after 2 failures. Include a 5-minute cooldown to prevent alert storms.

Can I check proxy anonymity level in the health monitor?

Yes. The httpbin.org/ip response includes the visible IP. Compare it to the proxy’s expected exit IP. If your real IP appears, the proxy is transparent. Add anonymity level as a tracked metric alongside latency and uptime.


Related Reading

Scroll to Top