npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@madhavmahajan132/nodepulse

v1.1.0

Published

Zero-configuration application performance monitoring for Node.js and Express.

Readme

NodePulse APM

npm version npm downloads Node.js version MIT license

A small, self-hosted performance dashboard for Node.js and Express.

NodePulse measures route traffic, response time, percentiles, errors, aborts, and unmatched requests directly inside your Express process. There is no monitoring account to create, no external collector to run, and no request-level data sent anywhere.

NodePulse dashboard overview with route metrics and traffic charts

Security: NodePulse does not include authentication. Protect the dashboard and JSON endpoint with your own middleware before exposing them outside a trusted environment.

What you get

  • A self-contained dashboard at /nodepulse
  • Searchable routes and expandable, interactive per-API traffic charts
  • Versioned JSON metrics at /nodepulse/metrics.json
  • Normalized METHOD /route/:pattern aggregation
  • Average requests per minute (RPM)
  • Average, minimum, and maximum response time
  • Histogram-derived p50, p95, and p99 latency
  • Configurable error-rate tracking
  • Aborted-request and unmatched-request tracking
  • Bounded memory through retention and route-cardinality limits
  • Dual ESM/CommonJS support with TypeScript declarations
  • No runtime dependencies other than your existing Express installation

Requirements

  • Node.js 22 or newer
  • Express 4.18 through Express 5.x

Installation

npm install @madhavmahajan132/nodepulse

Quick start

Mount NodePulse before request logging and application routes so it can observe the final response status and duration.

CommonJS

const express = require("express");
const nodepulse = require("@madhavmahajan132/nodepulse");

const app = express();

app.use(nodepulse());
app.use(express.json());

app.get("/api/users/:id", async (request, response) => {
  response.json({ id: request.params.id });
});

app.listen(3000);

ES modules

import express from "express";
import nodepulse from "@madhavmahajan132/nodepulse";

const app = express();

app.use(nodepulse());
app.use(express.json());

app.get("/api/users/:id", async (request, response) => {
  response.json({ id: request.params.id });
});

app.listen(3000);

Exercise a few application routes, then open:

Dashboard:    http://localhost:3000/nodepulse
JSON metrics: http://localhost:3000/nodepulse/metrics.json

With the default configuration, NodePulse needs one complete 60-second bucket before it displays aggregate metrics. Seeing Warming up during that first minute is expected.

Integration in an existing application

The recommended middleware order is:

const express = require("express");
const cors = require("cors");
const nodepulse = require("@madhavmahajan132/nodepulse");

const app = express();

app.disable("x-powered-by");

// Mount before request logging and application routes.
const pulse = nodepulse({ excludePaths: ["/health"] });
app.use(pulse);

app.use(cors());
app.use(express.json({ limit: "100kb" }));
app.use(requestLogger);

app.get("/health", healthHandler);
pulse.mount(app, "/api/users", usersRouter);
pulse.mount(app, "/api/orders", ordersRouter);

app.use(notFound);
app.use(errorHandler);

NodePulse listens for the response lifecycle, so an error status assigned by the final error handler is still recorded correctly.

Understanding the dashboard

Start with the traffic, response-time, and error summaries, then search routes by path or HTTP method. Filter for errors or aborts and sort by requests, p95 latency, or error rate to find an API worth investigating.

Select a route or its sparkline to expand its details. The larger chart shows completed time buckets with exact timestamps. Hover, tap, or use the keyboard-accessible slider to inspect request counts and RPM. The same view includes p50/p95/p99, minimum and maximum latency, completed responses, errors, and aborts. Latency percentiles summarize the selected window; the historical chart shows traffic.

Choose a shorter window, pause automatic updates, or refresh manually. The open route and inspected time stay selected during polling. Connection failures retain the last successful snapshot with a visible warning. The interface supports mobile screens and keyboard navigation; Escape closes route details.

Expanded API details with an interactive traffic chart, latency percentiles, errors, and aborts

View the mobile route details screenshot. These screenshots use actual completed traffic from the included example application with one-second demonstration buckets; the default bucket remains 60 seconds.

| Value | Meaning | | ------------------ | ------------------------------------------------------------------------- | | Tracked routes | Normalized route keys currently retained in memory. | | Total requests | Matched requests from completed buckets in the displayed window. | | Unmatched | Requests that did not resolve to an Express route, usually 404s. | | Avg RPM | Average matched requests per minute across completed buckets. | | Average | Mean response time for cleanly completed requests. | | p95 | Estimated latency at or below which 95% of completed requests finished. | | Errors | Percentage of completed responses meeting the configured error threshold. | | Recent trend | Request counts per completed time bucket, oldest to newest. |

Hover over or focus a dashboard column title for a short explanation inside the UI.

Complete-bucket reporting

NodePulse never mixes a partially active bucket with completed data.

With the default 60-second bucket:

Completed bucket 1    Completed bucket 2    Active bucket 3
10 requests           0 requests            7 requests so far
      │                      │                       │
      └──────────────────────┴── included            └── excluded

Every route metric—traffic, latency, percentiles, errors, aborts, and trends—uses the same completed buckets.

  • Values change only when a bucket closes.
  • No partial-bucket extrapolation is performed.
  • A completed zero-request bucket is real data and contributes to the RPM denominator.
  • A zero-request bucket does not invent latency or error samples; those values remain empty when no completed responses exist.

The dashboard defaults to the full retention window; the window selector offers shorter bucket-aligned periods. During the first hour it uses the completed portion available. For example, after 10 minutes:

{
  "windowSeconds": 3600,
  "effectiveWindowSeconds": 600
}

After 60 minutes, the window continuously rolls forward: the oldest completed bucket expires as a new one closes.

Configuration

app.use(
  nodepulse({
    retentionMinutes: 60,
    bucketSizeSeconds: 60,
    dashboardPath: "/nodepulse",
    metricsJsonPath: "/nodepulse/metrics.json",
    errorStatusThreshold: 500,
    maxTrackedRoutes: 200,
    excludePaths: ["/health", /^\/internal/],
  }),
);

| Option | Default | Description | | ---------------------- | ------------------------: | ------------------------------------------------------------------------------- | | retentionMinutes | 60 | Maximum completed history retained in memory. | | bucketSizeSeconds | 60 | Duration of one fixed time bucket. | | dashboardPath | /nodepulse | Path serving the browser dashboard. | | metricsJsonPath | /nodepulse/metrics.json | Path serving schema-v2 JSON metrics. | | errorStatusThreshold | 500 | Status code at or above which a response counts as an error. | | maxTrackedRoutes | 200 | Maximum distinct matched route keys before overflow uses other. | | excludePaths | [] | Exact strings or regular expressions matched against normalized route patterns. |

Invalid options throw when nodepulse() is initialized, allowing configuration mistakes to fail during application startup.

Important validation rules:

  • Retention in seconds must be evenly divisible by bucket size.
  • Dashboard and JSON paths must be distinct absolute paths.
  • Stateful regular expressions using g or y are rejected.
  • NodePulse automatically excludes its own dashboard and polling traffic.

Route aggregation

NodePulse groups requests by HTTP method and normalized Express route pattern:

GET /users/1      ┐
GET /users/42     ├──▶ GET /users/:id
GET /users/9382   ┘

POST /users/42   ─────▶ POST /users/:id

Methods remain separate because different methods often perform very different work.

Query parameters never change route identity. For a registered /users/:id route, /users/123?page=1 and /users/alice?sort=name both count as GET /users/:id. Parameter values are never used to guess the route pattern. Registered case, optional/wildcard syntax, and strict leaf trailing slashes are preserved.

Registering router mounts

Use the returned middleware's mount helper for every router mount in a chain, including static mounts:

const pulse = nodepulse();
app.use(pulse);

const members = express.Router();
members.get("/:memberId", memberHandler);

const teams = express.Router();
pulse.mount(teams, "/members", members);
pulse.mount(app, "/teams/:teamId", teams);

This records GET /teams/:teamId/members/:memberId for numbers, UUIDs, encoded IDs, and arbitrary slugs, with or without mergeParams. Ordinary app.get() route definitions stay unchanged, and child routes can be registered before the helper is called.

pulse.mount(parent, path, router) returns the parent. The parent may be an Express application or Router; the child must be an Express Router. String, RegExp, and array paths use the installed Express version's matching rules. Array alternatives from one registration share one metric key. Nested Express sub-applications are not supported by the helper.

Migration: replace app.use("/api", router) with pulse.mount(app, "/api", router), and do the same for nested mounts. Existing middleware calls still work, but a mount prefix not registered with the helper is reported in the reserved unresolved series. An empty root mount adds no prefix and can be transparent. Enabling mergeParams alone no longer supplies mount identity.

The unresolved series combines matched requests whose full pattern cannot be established. It has the same latency, error, and abort metrics as ordinary routes, occupies one bounded series outside maxTrackedRoutes, and never stores literal mount values. It is separate from other (route-cap overflow) and unmatched (no matched Express route). Path exclusions apply only to resolved patterns; unresolved traffic is not excluded by guessing a path.

Route identity is captured when Express selects a route, so it survives asynchronous responses and app-level error handlers. If several routes match, the last selected route wins. A route that calls next() and eventually falls through to a 404 remains attributed to that matched route.

Requests with no matched Express route are grouped under one fixed unmatched series. Unknown raw paths are not stored individually.

JSON metrics API

GET /nodepulse/metrics.json
GET /nodepulse/metrics.json?windowSeconds=300

windowSeconds must be positive, divisible by the configured bucket size, and no larger than retention.

A shortened schema-v2 response looks like:

{
  "schemaVersion": 2,
  "generatedAtMs": 1788060000000,
  "windowStartMs": 1788059400000,
  "windowEndMs": 1788060000000,
  "aggregationState": "ready",
  "windowSeconds": 3600,
  "effectiveWindowSeconds": 600,
  "bucketSizeSeconds": 60,
  "routes": [
    {
      "routeKey": "GET /api/users/:id",
      "aggregationState": "ready",
      "requestCount": 24,
      "completedCount": 23,
      "errorCount": 1,
      "abortedCount": 1,
      "requestsPerSecond": 0.04,
      "averageResponseTimeMs": 42.5,
      "errorRate": 0.0434782609,
      "p95ResponseTimeMs": 187.5,
      "p99ResponseTimeMs": 205.5,
      "recentRequestCounts": [3, 1, 0, 4, 0, 0, 0, 0, 7, 9]
    }
  ],
  "unmatched": {
    "routeKey": "unmatched",
    "aggregationState": "ready",
    "requestCount": 2,
    "statusCounts": { "404": 2 },
    "recentRequestCounts": [0, 0, 1, 0, 0, 0, 0, 0, 0, 1]
  }
}

Before the first bucket closes, aggregate fields are null and aggregationState is "warming_up".

windowStartMs is inclusive and windowEndMs is exclusive. They identify the exact completed interval, anchored to middleware startup. Trend item i starts at windowStartMs + i * bucketSizeSeconds * 1000. These additive fields preserve schema v2.

Both NodePulse endpoints accept only GET and HEAD. Other methods receive HTTP 405.

Protecting the dashboard

NodePulse deliberately does not prescribe an authentication system. Place your own authentication middleware before it:

app.use("/nodepulse", requireAuth);
app.use(nodepulse());

For local-only development, also bind the application to loopback:

app.listen(3000, "127.0.0.1");

The dashboard response includes restrictive content-security, framing, referrer, content-type, and cache headers. These headers do not replace authentication.

Storage and privacy

Metrics are held in bounded JavaScript Map objects inside the current Node.js process.

NodePulse stores aggregate counters and latency histograms. It does not store:

  • Request or response bodies
  • Headers, cookies, or authorization tokens
  • Raw parameter values
  • Individual request records
  • Metrics in a database or external service

Consequences of this design:

  • Metrics reset when the process restarts.
  • Each worker or server has its own independent dashboard.
  • No application metrics leave the process unless someone accesses the configured JSON endpoint.

Memory protection

  • Completed buckets automatically expire after retention.
  • The number of matched route keys is capped by maxTrackedRoutes.
  • Routes beyond the cap share the reserved other series.
  • Matched traffic with unknown identity shares one separate unresolved series.
  • Unmatched paths share one bounded series rather than using raw URLs.
  • Idle routes are removed after their last activity leaves retention.

The release memory gate simulates 3.6 million requests at 500 RPS across 50 routes and verifies that heap usage plateaus after retention fills. See benchmark methodology.

Current limitations

NodePulse v1 does not include:

  • Persistence across process restarts
  • Aggregation across clusters, workers, containers, or servers
  • Built-in authentication
  • Distributed tracing
  • Alert delivery
  • Exact percentiles based on retained raw samples
  • A public storage-adapter API

Documentation

Development

npm install
npm run check
npm run test:coverage
npm run test:browser

Additional benchmark and packaging commands are documented in MANUAL.md.

License

MIT