process-stats-sampler
v1.1.3
Published
Sample Node.js process memory and CPU usage to a JSON file
Maintainers
Readme
process-stats-sampler
Samples the Node.js process's memory and CPU usage into a JSON file and measures the Node event-loop execution delay. sample() takes one snapshot; call it on your own schedule (e.g. with setInterval) to build a time series.
This library is maintained primarily for personal use, so compatibility guarantees are pragmatic rather than semver-strict.
Install
npm install process-stats-samplerUsage
const {sample} = require('process-stats-sampler');
// Call every 30 seconds; the sample is written to /tmp/stats.json.
// sample() rejects on runtime failures (e.g. disk full), so handle errors
// instead of letting them become unhandled rejections.
setInterval(async () => {
try {
await sample('/tmp/stats.json');
} catch (error) {
console.error('sampling failed:', error.message);
}
}, 30_000);ESM
The package ships both require and import entry points. They share the same module instance, so mixing both in one process is safe:
import {sample, lag, reset} from 'process-stats-sampler';Measuring delay
const {lag} = require('process-stats-sampler');
// Waits 1000ms and returns the difference between the actual and expected time (ms, >= 0)
const delay = await lag(1000);lag(ms = 1000) measures the Node event-loop execution delay: when the event loop is blocked by synchronous work, timers fire late and the difference between the actual and expected elapsed time is the returned delay.
API
sample(filename?, options?)
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| filename | string | /tmp/stats.json | Output JSON file path; parent directories are created automatically. Writes use a temp file + atomic rename. Each call overwrites the file with the latest sample |
| options.unit | 'ratio' \| 'percent' \| 'machine-percent' | 'ratio' | CPU output unit |
| options.lag | boolean \| number | true | Record lag (event-loop delay probe): true probes with 1ms, false skips the probe (field is 0), a number sets a custom probe duration in ms |
Invalid arguments (filename empty, invalid unit/lag, invalid ms) throw a TypeError / RangeError. Runtime failures (e.g. file I/O errors) reject the returned promise with the underlying Error.
CPU units
ratio(default): CPU microseconds / wall-clock milliseconds between samples; a fully utilized core is about 1000percent: percent of one core; a fully utilized core is 100machine-percent: percent of the whole machine (percent of one core ÷ number of cores available to the process)
The lag field in the output is the event-loop execution delay probe (same semantics as lag()): each sample waits on a short timer; when the event loop is blocked by synchronous work the timer fires late, so the value is the current Node execution delay in ms (min 0). By default it probes with 1ms; use options.lag to disable it or set a custom probe duration.
lag(ms?)
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| ms | number | 1000 | Expected wait time in ms; a non-negative finite number up to ~24.8 days |
Returns the difference between the actual elapsed time and ms (ms, min 0).
Example output:
{
"rss": 41058304,
"heapTotal": 16777216,
"heapUsed": 8615928,
"external": 863268,
"arrayBuffers": 11358,
"user": 0.25,
"system": 0.06,
"lag": 0,
"timestamp": 1786320000000
}Behavior notes
- The CPU rate is the delta of
process.cpuUsage()between two consecutive samples of the same file divided by the actual wall-clock elapsed time (viaperformance.now()). The first sample of a file is 0 because it only establishes the baseline, and an irregular call cadence does not distort the reading. - The
timestamp, the memory snapshot and the CPU counters are all captured at the start of the sample, before the lag probe, so they are aligned with each other. userandsystemare JSON numbers rounded to 3 decimals (CPU µs per ms of wall time, or a percentage per the chosen unit).- Calls targeting the same file are serialized internally; different files run independently, each with its own CPU baseline and queue. Note that
process.cpuUsage()is process-wide, so overlapping streams each report the whole process's CPU; per-target attribution requires separate processes. machine-percentderives the available core count on Linux from the cgroup CPU quota (v2cpu.max/ v1cfs_quota_us) and the cpuset, taking the binding constraint and preserving fractional quotas (e.g. 0.5 core). It is re-read on every sample so runtime changes (docker update, HPA) are picked up. On other platforms it falls back toos.cpus().length.reset(filename)clears the sampling state (CPU baseline) for a file; the next sample starts fresh. State for each distinct filename is retained until reset, so callers using dynamic filenames should reset them when done.- File writes are atomic (temp file +
rename), so the target file is never left truncated. A hard kill between the write and the rename may leave an orphan temp file. - The
lagtimer is notunref()ed, so a process with only a pendinglagtimer stays alive until it fires (this guarantees the promise always resolves).
Changelog
See CHANGELOG.md for the full release history.
Development
npm run build # compile to dist/
npm test # build + run node:test tests