π Process load measuring plugin for Node.js with automatic "Service Unavailable" handling
A lightweight, production-ready middleware for monitoring Node.js application health and automatically handling server overload situations. Integrates seamlessly with popular frameworks (Koa, Express, Hono) and provides Prometheus-compatible metrics.
setTimeout instead of setInterval to minimize system pressurenpm install @stephen-shopopop/node-metrics
const Koa = require('koa');
const { underPressureKoaMiddleware } = require('@stephen-shopopop/node-metrics');
const app = new Koa();
app.use(underPressureKoaMiddleware({
appName: 'service-order',
maxEventLoopDelay: 1000,
maxHeapUsedBytes: 100000000,
maxRssBytes: 100000000,
maxEventLoopUtilization: 0.98,
retryAfter: 10,
webServerMetricsPort: 9090
}));
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
const express = require('express');
const { underPressureExpressMiddleware } = require('@stephen-shopopop/node-metrics');
const app = express();
const port = 3000;
app.use(underPressureExpressMiddleware({
appName: 'service-order',
maxEventLoopDelay: 1000,
maxHeapUsedBytes: 100000000,
maxRssBytes: 100000000,
maxEventLoopUtilization: 0.98,
retryAfter: 10,
webServerMetricsPort: 9090
}));
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
import { Hono } from 'hono';
import { underPressureHonoMiddleware } from '@stephen-shopopop/node-metrics';
const app = new Hono();
app.use('*', underPressureHonoMiddleware({
appName: 'service-order',
maxEventLoopDelay: 1000,
maxHeapUsedBytes: 100000000,
maxRssBytes: 100000000,
maxEventLoopUtilization: 0.98,
retryAfter: 10,
webServerMetricsPort: 9090
}));
app.get('/', (c) => c.text('Hello Hono!'));
export default app;
import { createServer } from 'node:http';
import { Metrics, isUnderPressure } from '@stephen-shopopop/node-metrics';
const metrics = Metrics.start({
appName: 'service-order',
sampleIntervalInMs: 1000,
resolution: 10,
webServerMetricsPort: 9090
});
const options = {
maxEventLoopDelay: 1000,
maxHeapUsedBytes: 100000000,
maxRssBytes: 100000000,
maxEventLoopUtilization: 0.98
};
const hostname = '127.0.0.1';
const port = 3000;
const server = createServer((req, res) => {
if (isUnderPressure({ ...options, ...metrics.measures() })) {
res.statusCode = 503;
res.setHeader('Retry-After', '10');
res.end('Service Unavailable');
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
You can set up metrics separately and preload them when starting your application:
// file: metrics.js
import { Metrics } from '@stephen-shopopop/node-metrics';
const metrics = Metrics.start({
webServerMetricsPort: 9090,
appName: 'service-test'
});
process.on('SIGTERM', () => {
metrics
.closeWebServerMetrics()
.then(() => console.log('Metrics terminated'))
.catch((error) => console.error('Error terminating metrics', error))
.finally(() => process.exit(0));
});
Run your application with metrics preloaded:
node -r ./metrics.js app.js
| Option | Type | Default | Description |
|---|---|---|---|
appName |
string |
- | Required. Application name (format: service-name) |
sampleIntervalInMs |
number |
1000 |
Interval in milliseconds for metrics collection |
resolution |
number |
10 |
Resolution/granularity of collected metrics |
webServerMetricsPort |
number |
- | Port for Prometheus metrics server (recommended: 9090) |
maxEventLoopDelay |
number |
- | Maximum allowed event loop delay (ms) |
maxEventLoopUtilization |
number |
- | Maximum event loop utilization (0-1, e.g., 0.98 = 98%) |
maxHeapUsedBytes |
number |
- | Maximum heap memory usage in bytes |
maxRssBytes |
number |
- | Maximum Resident Set Size in bytes |
retryAfter |
number |
- | Seconds to wait before retrying (sent in Retry-After header) |
{
appName: 'service-order',
sampleIntervalInMs: 1000, // Collect metrics every second
resolution: 10, // Keep last 10 samples
maxEventLoopDelay: 1000, // Alert if event loop delay > 1000ms
maxEventLoopUtilization: 0.98, // Alert if event loop > 98% utilized
maxHeapUsedBytes: 100000000, // Alert if heap > 100MB
maxRssBytes: 100000000, // Alert if RSS > 100MB
retryAfter: 10, // Tell clients to retry after 10s
webServerMetricsPort: 9090 // Serve metrics on port 9090
}
Access the beautiful real-time dashboard at:
http://127.0.0.1:9090

Metrics are exposed in Prometheus format at:
http://127.0.0.1:9090/metrics
Available Metrics:
# HELP nodejs_event_loop_delay_milliseconds The mean of the recorded event loop delays
# TYPE nodejs_event_loop_delay_milliseconds gauge
nodejs_event_loop_delay_milliseconds{service="service-order"} 0.9878575824175826
# HELP nodejs_event_loop_utilized The percentage of event loop utilization
# TYPE nodejs_event_loop_utilized gauge
nodejs_event_loop_utilized{service="service-order"} 0.10445105761836926
# HELP nodejs_heap_used_bytes The amount of memory used by the V8 heap
# TYPE nodejs_heap_used_bytes gauge
nodejs_heap_used_bytes{service="service-order"} 32637488
# HELP nodejs_heap_total_bytes The total size of the V8 heap
# TYPE nodejs_heap_total_bytes gauge
nodejs_heap_total_bytes{service="service-order"} 34684928
# HELP nodejs_rss_bytes The resident set size, or total memory allocated for the process
# TYPE nodejs_rss_bytes gauge
nodejs_rss_bytes{service="service-order"} 179077120
# HELP nodejs_process_start_time_seconds The process start time, represented in seconds since the Unix epoch
# TYPE nodejs_process_start_time_seconds gauge
nodejs_process_start_time_seconds{service="service-order"} 1750345329
# HELP nodejs_process_cpu_user_seconds_total The total user CPU time consumed by the process, in seconds
# TYPE nodejs_process_cpu_user_seconds_total counter
nodejs_process_cpu_user_seconds_total{service="service-order"} 1.494779
# HELP nodejs_process_cpu_system_seconds_total The total system CPU time consumed by the process, in seconds
# TYPE nodejs_process_cpu_system_seconds_total counter
nodejs_process_cpu_system_seconds_total{service="service-order"} 0.120983
# HELP nodejs_process_cpu_seconds_total The total CPU time (user + system) consumed by the process, in seconds
# TYPE nodejs_process_cpu_seconds_total counter
nodejs_process_cpu_seconds_total{service="service-order"} 1.615762
# HELP nodejs_active_handles Number of active libuv handles grouped by handle type
# TYPE nodejs_active_handles gauge
nodejs_active_handles{service="service-order",type="WriteStream"} 2
nodejs_active_handles{service="service-order",type="ReadStream"} 1
nodejs_active_handles{service="service-order",type="Server"} 1
nodejs_active_handles{service="service-order",type="Socket"} 1
Under the hood, node-metrics uses setTimeout to perform polling checks instead of setInterval. This design choice is intentional to avoid adding additional pressure to an already stressed system.
Why setTimeout?
setInterval calls repeatedly at scheduled intervals regardless of whether the previous call completed, potentially piling up calls when the server is under loadsetTimeout is called only once and reschedules itself after completion, preventing call accumulationNote: The two methods are not identical in behavior. The timer function is not guaranteed to run at the exact same rate when the system is under pressure or running long processes, but this trade-off is acceptable for health monitoring purposes.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β APPLICATION LAYER β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Koa β β Express β β Hono β β
β β Application β β Application β β Application β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β β β β β
β βββββββββββββββββββββ΄ββββββββββββββββββββ β
β β β
β ββββββββββββββββΌβββββββββββββββ β
β β Middleware Integration β β
β β underPressure*Middleware() β β
β ββββββββββββββββ¬βββββββββββββββ β
βββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β CORE LAYER β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Metrics (Singleton) β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β start() β register() β measures() β destroy()β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β βββββββββββββββ ββββββββββββββββ β β
β β β Timer ββββββββΆβ Mediator β β β
β β β(setTimeout) β β (Plugins) β β β
β β βββββββββββββββ ββββββββ¬ββββββββ β β
β β β β β
β β βββββββββββββββββββββββββββββΌβββββββββββββββββββββββββ β β
β β β StoreBuilder (Metrics Storage) β β β
β β βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββ β β
β β β β β
β β βββββββββββββββββββββββββββββΌβββββββββββββββββββββββββ β β
β β β BroadcastChannel (Pub/Sub) β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ β
β β Metrics Server β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β β
β β β GET / β β GET /metrics β β GET /metrics-stream β β β
β β β Dashboard β β Prometheus β β SSE Stream β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β PLUGIN LAYER β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Memory β β Event Loop β β Event Loop β β
β β Usage β β Delay β β Utilization β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Process CPU β β Process β β Active Handles/ β β
β β Usage β β Uptime β β Resources β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Request Flow with Pressure Detection:
βββββββββββββββ
β HTTP β
β Request β
ββββββββ¬βββββββ
β
βΌ
ββββββββββββββββββββ
β Middleware β
β Check β
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββ
β measures() β
β Get current β
β metrics β
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β isUnderPressure() β
β ββββββββββββββββββββββββββ β
β β Event Loop Delay > max?β β
β β Heap Usage > max? β β
β β RSS Memory > max? β β
β β EL Utilization > max? β β
β ββββββββββββββββββββββββββ β
ββββββββ¬ββββββββββββββ¬ββββββββββ
β β
Yes β β No
βΌ βΌ
βββββββββββββββ ββββββββββββββββ
β Return 503 β β Continue to β
β Service β β next() β
β Unavailableβ β β
β Retry-Afterβ β β
βββββββββββββββ ββββββββββββββββ
β±οΈ Periodic Metrics Collection (Every sampleIntervalInMs):
ββββββββββββββββ
β setTimeout β
β Trigger β
ββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββββββ
β Mediator β
β Iterate Plugins β
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Each Plugin.capture(StoreBuilder) β
β ββββββββββββββββββββββββββββββββββββββ β
β β MemoryUsagePlugin β β
β β β heap_used_bytes β β
β β β rss_bytes β β
β ββββββββββββββββββββββββββββββββββββββ€ β
β β EventLoopDelayPlugin β β
β β β event_loop_delay_milliseconds β β
β ββββββββββββββββββββββββββββββββββββββ€ β
β β EventLoopUtilizationPlugin β β
β β β event_loop_utilized β β
β ββββββββββββββββββββββββββββββββββββββ€ β
β β ProcessCpuUsagePlugin β β
β β β process_cpu_*_seconds_total β β
β ββββββββββββββββββββββββββββββββββββββ β
ββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββ
β StoreBuilder β
β Update Metrics β
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββ
β BroadcastChannel β
β Publish Update β
ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β Subscribers (Dashboard, β
β Metrics Stream, etc.) β
ββββββββββββββββββββββββββββββββ
For detailed API documentation and advanced usage:
Contributions are welcome! Please feel free to submit a Pull Request.
Made with β€οΈ by Stephen Deletang