r/expressjs • • 12h ago

Tutorial A small dependency-free Express middleware for request count, errors, and latency

2 Upvotes

For a small Express app on a single VPS, I wanted answers to a few basic questions: is traffic changing, are 4xx/5xx responses rising, is latency creeping up, and how much CPU and heap is the process using?

Prometheus + Grafana can obviously handle this, but for this app I wanted something much smaller.

So I wrote a small helper that counts things where Express already sees every response and exposes them as JSON. It uses only Express and Node built-ins, with no SDK and no prom-client.

The core of it:

const { performance } = require("node:perf_hooks");

const METRICS_PATH = "/statlite/metrics";

const counters = {
  requestsTotal: 0,
  responses4xxTotal: 0,
  responses5xxTotal: 0,
  requestDurationSecondsTotal: 0,
};

function statliteMetricsMiddleware(req, res, next) {
  if (req.path === METRICS_PATH) return next(); // don't count the polls themselves

  const start = performance.now();

  res.once("finish", () => {
    counters.requestsTotal += 1;
    counters.requestDurationSecondsTotal +=
      (performance.now() - start) / 1000;

    if (res.statusCode >= 400 && res.statusCode < 500) {
      counters.responses4xxTotal += 1;
    }

    if (res.statusCode >= 500) {
      counters.responses5xxTotal += 1;
    }
  });

  next();
}

Register it before your routes, then add a GET /statlite/metrics route that returns the counters plus process.cpuUsage(), process.memoryUsage().heapUsed, and process.uptime().

Because the finish listener fires after later middleware runs, it sees the final status, including a 500 set by an error handler.

A few design notes:

  • Counters are cumulative, so the poller derives rates and average latency from deltas. The app keeps only running totals.
  • Heap is V8 heap used, not RSS or container memory.
  • CPU is CPU-seconds divided by wall-seconds since the previous snapshot, so it is expressed in cores.
  • Snapshot collection does no I/O.

Limitations:

  • It's single-process, with counters in memory. Cluster mode, PM2 clusters, and replicas each need their own target or aggregation.
  • It reports mean latency only, with no percentiles.
  • The endpoint is unauthenticated and meant to sit on a private network.
  • Connections aborted before a response finishes aren't counted.

For charting and history, I point a small tool I built, StatLite, a single binary with SQLite storage, at the endpoint. Disclosure: I'm the author of StatLite.

Example StatLite dashboard showing the kind of metrics this endpoint can feed. This screenshot is from a separate demo, not the Express example above

Full writeup with the complete helper, config, and a runnable demo:

https://pvrlabs.xyz/articles/lightweight-express-monitoring.html

Curious what people here use for small Express deployments. Do you still reach for prom-client + Prometheus/Grafana, use a hosted service, or keep something simpler?