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-cronCore 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-monitorInternal Links
- Building a Proxy Checker Tool — one-time proxy checking
- Creating a Web Scraping Dashboard with Grafana — visualize with Grafana
- Proxy Pool Manager: Open Source Guide — manage proxy pools
- Proxy Log Analyzer — analyze historical logs
- Building a Proxy Chain Validator — validate proxy chains
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.
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a Proxy Rotator in Python: Complete Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Bandwidth Optimization for Proxies: Reduce Costs & Increase Speed
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a Proxy Rotator in Python: Complete Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Bandwidth Optimization for Proxies: Reduce Costs & Increase Speed
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a News Crawler in Python: Step-by-Step Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Azure Functions for Serverless Web Scraping: the Complete Guide
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)
Related Reading
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a News Crawler in Python: Step-by-Step Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Azure Functions for Serverless Web Scraping: the Complete Guide
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)