@chhax1618/probe
v1.0.0
Published
Lightweight, zero-dependency Express middleware for server latency profiling — p50/p95/p99, slow-request capture, event-loop lag, GC tracking.
Maintainers
Readme
probe
Lightweight, zero-dependency Express middleware for server latency profiling.
What it gives you
| Feature | Description |
|---|---|
| Per-route percentiles | p50 / p95 / p99 / max / avg, sorted by p95 desc |
| Slow-request capture | Full context dump: concurrent reqs, event-loop lag, memory, correlation ID |
| Queue-time detection | Measures nginx/LB → app delay from X-Request-Start header |
| Server diagnostics | CPU load, memory pressure, heap %, uptime — with auto-verdict |
| Event-loop lag | monitorEventLoopDelay histogram, reset per slow event |
| GC pause tracking | PerformanceObserver on gc entries, correlated to slow requests |
| Downstream isolation | Wrap DB/API calls to see if lag is yours or a dependency |
| Correlation ID | Auto-generates/propagates x-request-id |
| < 0.1ms overhead | Typed-array ring buffer, no allocs in hot path |
| Zero dependencies | Single file, ~260 lines |
Install
# npm (once published)
npm i @chhax1618/probe
# or just copy index.js into your project
cp index.js /your/project/lib/probe.jsQuick Start
const express = require('express');
const createProbe = require('@chhax1618/probe'); // or './lib/probe'
const probe = createProbe({ slowThresholdMs: 300 });
const app = express();
// Mount FIRST — before body parsers, auth, etc.
app.use(probe.middleware);
// Report endpoint (add auth in production!)
app.get('/latency-report', probe.reportRoute);
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }));
app.listen(3000);Nginx Setup (for queue-time detection)
Add this to your nginx location block:
proxy_set_header X-Request-Start "t=${msec}";The probe will automatically detect this header and calculate how long the request sat in nginx/LB before reaching your app. No code changes needed.
API
createProbe(options?)
| Option | Default | Description |
|---|---|---|
| slowThresholdMs | 500 | Requests above this are flagged slow |
| ringSize | 500 | Max samples per route (ring buffer) |
| reportAuth | null | fn(req) → bool gate for report endpoint |
| enableGC | true | Track GC pause durations |
| enableCorrelationId | true | Auto x-request-id |
| enableQueueTime | true | Detect nginx/LB queue time |
| enableServerInfo | true | Include server diagnostics in full report |
| instanceId | $HOSTNAME | Tag for multi-instance deploys |
| onSlow | console.warn | Custom handler fn(slowCtx) |
Instance methods
| Method | Returns |
|---|---|
| probe.middleware | Express middleware — mount first |
| probe.report() | Route stats array (sorted by p95 desc) |
| probe.summary() | Quick health-check object |
| probe.serverInfo() | Full server diagnostics + verdict |
| probe.queueReport() | Nginx/LB queue time percentiles |
| probe.reportRoute | Express handler — mount on /latency-report |
| probe.wrapCall(label, fn) | Returns wrapped async fn with latency tracking |
| probe.downstreamReport() | Stats for wrapped downstream calls |
| probe.gcReport() | Recent GC pause entries |
| probe.reset() | Clear all stats |
Expected Output
GET /latency-report — standard report
{
"summary": {
"uptimeSec": 3600.12,
"totalRequests": 48210,
"inFlight": 3,
"routeCount": 12,
"globalP95Ms": 245.8,
"slowTotal": 47,
"queueTime": { "count": 48210, "avgMs": 1.2, "p50Ms": 0.8, "p95Ms": 3.1, "p99Ms": 8.4, "maxMs": 42 },
"eventLoopLagMs": 0.45,
"memoryRssMb": 128.3
},
"routes": [
{ "route": "GET /orders/:id", "count": 8420, "avgMs": 45.2, "p50Ms": 32.1, "p95Ms": 245.8, "p99Ms": 412.3, "maxMs": 890.1, "slowCount": 47 },
{ "route": "POST /users", "count": 3200, "avgMs": 12.4, "p50Ms": 8.9, "p95Ms": 38.2, "p99Ms": 62.1, "maxMs": 120.5, "slowCount": 0 }
],
"queueTime": { "count": 48210, "avgMs": 1.2, "p50Ms": 0.8, "p95Ms": 3.1, "p99Ms": 8.4, "maxMs": 42 },
"downstream": [
{ "label": "db:findOrder", "count": 8420, "avgMs": 40.1, "p50Ms": 28.0, "p95Ms": 210.3, "p99Ms": 380.1, "maxMs": 850.2, "slowCount": 35 },
{ "label": "cache:get", "count": 12000, "avgMs": 0.8, "p50Ms": 0.5, "p95Ms": 1.9, "p99Ms": 3.2, "maxMs": 12.1, "slowCount": 0 }
]
}GET /latency-report?full — includes server diagnostics + GC
{
"summary": { "..." : "same as above" },
"routes": [ "..." ],
"downstream": [ "..." ],
"gc": [
{ "ts": 1720267200000, "ms": 4.21, "kind": 1 },
{ "ts": 1720267260000, "ms": 12.8, "kind": 2 }
],
"server": {
"platform": "linux",
"arch": "x64",
"nodeVersion": "v20.19.1",
"pid": 12345,
"uptime": { "processSec": 3600.12, "systemSec": 864000.5 },
"cpu": {
"cores": 4,
"model": "Intel Xeon E5-2686 v4",
"loadAvg1m": 2.8,
"loadAvg5m": 2.1,
"loadAvg15m": 1.6,
"loadPerCore1m": 0.7,
"usagePct": 68.4
},
"memory": {
"system": { "totalMb": 8192, "freeMb": 1024, "usedPct": 87.5 },
"process": { "rssMb": 128.3, "heapUsedMb": 82.1, "heapTotalMb": 96.0, "externalMb": 4.2, "heapPct": 85.5 }
},
"verdict": ["CPU_HIGH", "MEMORY_HIGH", "HEAP_PRESSURE"]
}
}Slow-request log (automatic on console.warn)
{
"route": "GET /orders/:id",
"method": "GET",
"status": 200,
"durationMs": 812.34,
"queueTimeMs": 3.2,
"totalTimeMs": 815.54,
"timestamp": "2026-07-06T12:00:00.000Z",
"concurrentAtStart": 14,
"memoryRssMb": 128.3,
"eventLoopLagMs": 4.21,
"eventLoopLagMaxMs": 18.90,
"requestId": "abc-123"
}How to Read the Output
Where is the latency coming from?
| Symptom | Look at | Likely cause |
|---|---|---|
| High queueTimeMs, low durationMs | Queue time report | Nginx/LB overloaded — not your app |
| High durationMs, high downstream p95 | Downstream report | DB or external API is slow |
| High durationMs, low downstream p95 | Event loop lag | Your code is blocking (CPU, sync ops) |
| High eventLoopLagMs on slow requests | Slow-request logs | Sync blocking code — JSON.parse, regex, etc. |
| Slow requests cluster after GC pauses | GC report timestamps | Memory pressure / leak |
| verdict: CPU_SATURATED | Server info | Server overloaded — scale up/out |
| verdict: MEMORY_PRESSURE | Server info | Swapping — reduce memory or scale |
| verdict: HEAP_PRESSURE | Server info | GC thrashing — memory leak likely |
Verdict flags
| Flag | Meaning |
|---|---|
| HEALTHY | All good |
| CPU_HIGH | Load per core > 0.7 |
| CPU_SATURATED | Load per core > 1.0 — requests will queue |
| MEMORY_HIGH | System memory > 75% used |
| MEMORY_PRESSURE | System memory > 90% — swap likely |
| HEAP_PRESSURE | V8 heap > 85% — GC overhead increasing |
Downstream Call Wrapping
// Wrap any async function to track its latency separately
const findOrder = probe.wrapCall('db:findOrder', async (id) => {
return db.orders.findById(id);
});
const callPaymentAPI = probe.wrapCall('http:payments', async (data) => {
return fetch('https://api.payments.com/charge', { method: 'POST', body: JSON.stringify(data) });
});
// Use them normally — latency is tracked automatically
app.get('/orders/:id', async (req, res) => {
const order = await findOrder(req.params.id);
res.json(order);
});Caveats
- In-memory only — resets on restart. Call
probe.report()on an interval and persist the JSON for history. - Per-instance — in clustered/multi-pod setups, each instance has its own stats. Use
instanceIdand aggregate externally. - Queue time requires header — configure nginx with
proxy_set_header X-Request-Start "t=${msec}";
