DEV Community

Cover image for Pulse: real memory and health monitoring for Node.js, no PM2 required
Dmitry Sheiko
Dmitry Sheiko

Posted on

Pulse: real memory and health monitoring for Node.js, no PM2 required

If you've run a Node app under PM2 for more than a few months, you've probably
seen this pattern: a monitoring script shells out to pm2 jlist, walks
/proc/[pid] for every worker, and estimates heap usage as something like
rss * 0.6 / 0.75. It works well enough to put a number on a dashboard, but
it's a guess. Nothing in that pipeline can actually see what V8 thinks its
heap looks like from outside the process.

Usually there's no real alerting behind it either. Someone has to be
watching the graph when memory starts climbing, and by the time they notice,
the process is already restarting itself in a loop.

I got tired of rebuilding this half-working setup for every project, so I
built Pulse, an open source memory and
health monitoring package for Node.js. It's built with Next.js in mind but
doesn't depend on it.

What it actually does differently

Real metrics instead of estimates. Pulse runs inside the process it's
monitoring and reads process.memoryUsage(), process.cpuUsage(), and
v8.getHeapStatistics() directly. No RSS-to-heap conversion formula, no
guessing. A sample looks like this:

interface Sample {
  timestamp: number;
  rssMB: number;
  heapUsedMB: number;
  heapTotalMB: number;
  externalMB: number;
  arrayBuffersMB: number;
  heapUsedPct: number; // heapUsed / heap size limit, 0-100
  cpuPercent: number;  // single-process CPU, not divided by core count
}
Enter fullscreen mode Exit fullscreen mode

Alerting that's actually wired up. You set thresholds per metric
(rssMB, heapUsedPct, cpuPercent), and Pulse debounces sustained
breaches so it fires once per episode instead of once per sample, then
sends a resolved notification when the metric comes back down. Console,
Webhook, Slack, and Discord notifiers ship out of the box.

No PM2 dependency. Pulse hooks into Next.js through
instrumentation.ts, so there's nothing to shell out to and nothing to
scrape from /proc. It works cross-platform because it's just reading from
the Node process itself.

Zero required infrastructure. SQLite by default, so you can add
monitoring to a side project without standing up a database. Postgres and
MySQL adapters are there if you're already running one.

Auth isn't an afterthought. The metrics route handler takes a required
authorize() callback with no default. You have to make an explicit
decision about who can hit the endpoint. It's not possible to accidentally
ship an open metrics route because you forgot to add auth later.

Using it in a Next.js app

npm install @pulse/next @pulse/core
Enter fullscreen mode Exit fullscreen mode

Register the collector in instrumentation.ts:

// instrumentation.ts
export async function register() {
  // instrumentation.ts is compiled for both the Node.js and Edge runtimes.
  // @pulse/core needs Node builtins, so keep it out of the Edge bundle.
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { startPulse } = await import("@pulse/next");
    startPulse({ storage: "sqlite", intervalMs: 30_000 });
  }
}
Enter fullscreen mode Exit fullscreen mode

Expose the metrics behind your own auth:

// app/api/pulse/route.ts
import { createMetricsRoute } from "@pulse/next";

export const { GET } = createMetricsRoute({
  authorize: async (request) => {
    const session = await getSession(request);
    return Boolean(session?.isAdmin);
  },
});
Enter fullscreen mode Exit fullscreen mode

And render the dashboard wherever you want to look at it:

// app/admin/pulse/page.tsx
import { PulseDashboard } from "@pulse/next";

export default function PulseAdminPage() {
  return <PulseDashboard endpoint="/api/pulse" refreshMs={15_000} />;
}
Enter fullscreen mode Exit fullscreen mode

Or skip writing those files by hand and run npx pulse init --storage sqlite.

What's in the repo

Pulse is an npm workspaces monorepo, no turborepo or other orchestrator on
top:

Package What it is
@pulse/core Framework-agnostic collector, storage adapters (sqlite/postgres/mysql), alert manager, retention, crash safety. No Next.js or React dependency.
@pulse/next Next.js glue: startPulse(), createMetricsRoute(), and a <PulseDashboard /> component.
@pulse/pm2 An escape hatch, not the default path. For processes you genuinely can't instrument directly: shells out to pm2 jlist and reads /proc. Linux-only.
pulse (CLI) pulse init scaffolds the files above, pulse view gives you a live terminal view of recent samples.

There are two working examples in the repo: a minimal Next.js App Router app
with SQLite storage and an intentionally leaky route for testing alerts, and
the same app wired to MySQL with a docker-compose.yml for local testing.

Where it's at

The core, the Next.js integration, and the CLI are all built with
TypeScript, bundled with tsup (CJS + ESM + .d.ts), and tested with
Vitest. It's early, so I'm mainly looking for feedback on the alerting
defaults and whether the storage adapter interface makes sense for people
running something other than SQLite.

Repo: github.com/dsheiko/pulse

If you've built your own version of the pm2 jlist + /proc dance, I'd
like to hear what pushed you to do it and whether Pulse would have covered
your case.

Top comments (0)