Export Prometheus Metrics with a Default Registry
Register a counter and a histogram with the default registry, instrument the request path, and expose /metrics in the text format Prometheus scrapes.
Task
Expose a /metrics endpoint that Prometheus can scrape, with a request counter and a latency histogram that have stable labels.
When to use this
The team wants a request rate and a latency percentile on the dashboard, but the app has no metrics endpoint. They need one that Prometheus scrapes without extra glue.
Prerequisites
- Node.js 20 LTS with prom-client installed
- An HTTP server that can add a /metrics route
- A Prometheus scrape config pointing at the app port
Steps
1. Register a counter for requests and a histogram for latency with method and route labels.
const requests = new Counter({ name: "http_requests_total", help: "Total requests", labelNames: ["method", "route"] });
const latency = new Histogram({ name: "http_request_duration_seconds", help: "Latency", labelNames: ["method", "route"], buckets: [0.01, 0.1, 1] });
Expected output: The metrics appear in the default registry with the declared names and labels.
2. Instrument the request path to observe the counter and the histogram on every request.
const end = latency.startTimer();
requests.inc({ method, route });
res.on("finish", () => end());
Expected output: A request increments the counter and records a latency sample in the histogram bucket.
3. Expose /metrics in the text format on the app port.
app.get("/metrics", async (req, res) => res.set("Content-Type", register.contentType).end(await register.metrics()));Expected output: A GET /metrics returns the Prometheus text format with the counter and histogram series.
Troubleshooting
Problem
Cardinality explodes because route includes the id
Solution
Use a low-cardinality route label like /users/:id, not the raw path; a unique label per request makes Prometheus unusable.
Problem
The scrape times out
Solution
Collecting metrics is single-threaded; if the registry is huge, move expensive collectors to a separate registry and scrape it less often.
Limitations
- The default registry is process-global; a long-running process accumulates series that must be reset on restart.
- Histogram buckets are fixed at registration; changing them requires a redeploy.
Register, instrument, expose
Register a counter and a histogram on the default registry, instrument the request path, and expose /metrics in the text format. Prometheus scrapes the endpoint on its interval.
const requests = new Counter({ name: 'http_requests_total', help: 'Total requests', labelNames: ['method', 'route'] });
const latency = new Histogram({ name: 'http_request_duration_seconds', help: 'Latency', labelNames: ['method', 'route'], buckets: [0.01, 0.1, 1] });
Keep labels low-cardinality
Use a templated route like /users/:id as the label, not the raw path. A unique label per request makes the series count explode and exhausts Prometheus memory.
app.get('/metrics', async (req, res) => res.set('Content-Type', register.contentType).end(await register.metrics()));
Expected outcome
A GET /metrics returns the text format with the counter and histogram series, the dashboard shows a request rate and a latency percentile, and the label cardinality stays bounded. Scrape the endpoint on a fixed interval and alert on the rate and the percentile.