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
}
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
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 });
}
}
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);
},
});
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} />;
}
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)