Open-source project
soyuka/pidusage avatar
soyuka/pidusage

pidusage measures a process you do not control, without native bindings

Cross-platform process cpu % and memory usage of a PID

547 stars58 forksJavaScriptMIT

At a glance

What is it?
pidusage answers a narrow question well: what is the CPU percentage and memory footprint of a given process ID, on Linux, macOS, Windows and Alpine. It reads the operating system's own process information rather than doing anything clever, so it works on a process that is not a Node program and on a machine where you cannot add instrumentation.
Who is it for?
Adopt pidusage if you need to measure a process you do not control, since it reads the operating system's own process data and needs no native compilation. Skip it if you are measuring your own Node.js process, because process.cpuUsage and process.memoryUsage do that better with no dependency at all.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A small job with a clear boundary

The package does one thing. Cross-platform process cpu % and memory usage of a PID.

That is the whole scope, and it is worth stating up front because a package this focused raises the question of what it does better than the alternatives.

The synopsis gives the answer in one line. The ideas come from node-usage, but with no C-bindings.

That is the whole design decision. The obvious way to read process statistics portably is to bind to the native API of each platform, which is what node-usage does, and which means a compiler toolchain, a build step and a binary per platform. pidusage avoids that by using the command line tools the operating system already ships, so it installs anywhere Node runs.

The cost of that choice is that it shells out, and the cost of shelling out is accuracy and speed. The compatibility table later in the documentation is honest about where the readings are imperfect.

The documentation also says something useful about when not to use the package. If you need to check a Node.js script's CPU and memory usage, you can use process.cpuUsage and process.memoryUsage, since Node v6.1.0. And then the reason this package still exists: it remains useful when you have no control over the remote script, or if the process is not a Node.js process.

That is the real use case. You are writing a supervisor, a monitoring agent or a test harness, and the thing you want to measure is a worker you launched or a process somebody else launched. In that case the process is opaque to you, there is no in-process API to call, and the only way to learn what it is doing is to ask the operating system.

The project is JavaScript, MIT licensed, with the package at version 4.1.0, requiring Node 18 or newer, and the last push on 2026-10-13.

The API and the shape of the answer

The usage example opens like this.

js
var pidusage = require('pidusage')

pidusage(process.pid, function (err, stats) {
  console.log(stats)
  // => {
  //   cpu: 10.0,            // percentage (from 0 to 100*vcore)
  //   memory: 357306368,    // bytes
  //   ppid: 312,            // PPID
  //   pid: 727,             // PID
  //   ctime: 867000,        // ms user + system time
  //   elapsed: 6650000,     // ms since the start of the process
  //   timestamp: 864000000  // ms since epoch
  // }
  cb()
})

The shape of the returned object is where the design shows. Seven fields, and each one is something a caller genuinely needs.

The cpu field is a percentage from 0 to 100 times the number of cores, which is the convention that lets a process use more than 100 percent when it runs on more than one core. The memory field is in bytes. The ppid field is the parent process ID, and the pid field is the one you asked about.

Then the two time fields, and the distinction between them matters. ctime is the user plus system time consumed, in milliseconds, and elapsed is the milliseconds since the start of the process. So ctime tells you how much work the process has done and elapsed tells you how long it has been alive, and the ratio of the two is roughly its average utilisation.

The timestamp field is milliseconds since the epoch, which is there so that a caller collecting samples can tell when a reading was taken. Without it, a series of samples has no time base.

Multiple process IDs are supported, and the shape of the result changes accordingly: pass an array and you get an object keyed by PID rather than a single stats object. The documentation shows this with two processes, one at 10 percent CPU and 357 megabytes and the other at 0.1 percent and about 3.8 megabytes, which is the shape of a parent process supervising a small child.

And the API is dual-mode. If no callback is given it returns a promise instead, which is what the example shows with a single await. The formal signature is pidusage(pids, options, callback) returning a Promise of an object only when the callback is not provided.

The pids parameter accepts a number, an array of numbers, a string or an array of strings, so you can pass identifiers as strings without converting them first.

The polling advice is the most important line in the example

There is a warning in the usage section that is easy to skim and that changes how you should write the calling code.

Avoid using setInterval as they could overlap with asynchronous processing.

The reason is not hard to see. The measurement is asynchronous, because it involves running a command and reading its output. A setInterval fires on a schedule regardless of whether the previous call has finished, so on a loaded machine the calls stack up. Each one spawns a subprocess, and the overlapping set of subprocesses gives you a reading that reflects several samples at once rather than one.

The documented workaround is to chain setTimeout calls instead, so the next call is scheduled from the completion of the previous one.

js
function compute(cb) {
  pidusage(process.pid, function (err, stats) {
    console.log(stats)
    // => {
    //   cpu: 10.0,            // percentage (from 0 to 100*vcore)
    //   memory: 357306368,    // bytes
    //   ppid: 312,            // PPID
    //   pid: 727,             // PID
    //   ctime: 867000,        // ms user + system time
    //   elapsed: 6650000,     // ms since the start of the process
    //   timestamp: 864000000  // ms since epoch
    // }
    cb()
  })
}

function interval(time) {
  setTimeout(function() {
    compute(function() {
      interval(time)
    })
  }, time)
}

// Compute statistics every second:
interval(1000)

And the same thing written with async and await:

js
const compute = async () => {
  const stats = await pidusage(process.pid)
  // do something
}

// Compute statistics every second:
const interval = async (time) => {
  setTimeout(async () => {
    await compute()
    interval(time)
  }, time)
}

interval(1000)

Both examples compute the statistics every second, and both guarantee that only one call is in flight. The async version is the shorter one to read, and the callback version is the one that works the same way in older code.

The second half of the point is about what the interval means. Because the interval is scheduled after the previous read completes, the actual sampling period is the configured time plus however long the read took. On a machine where reading process statistics is slow, the effective period drifts longer than a second. That is the correct behaviour for this use case, since a slightly stale but non-overlapping reading is more useful than a fresh one that is wrong.

There is also a cleanup function, pidusage.clear(), for deleting all in-memory metrics and clearing the event loop. The documentation is clear that it is not necessary before exiting, because the interval registered does not hold up the event loop. That is a small detail worth knowing, since it tells you that the library does not need explicit teardown to let a Node process finish.

Options, environment variables and the ps fallback

There are three options, and each one has an environment variable that does the same thing, with the rule that setting the option programmatically overrides the environment.

The first is usePs. By default, pidusage will use procfile parsing on most Unix systems, and if you want to use ps instead, set the option:

code
pidusage(pid, {usePs: true})

The default is false, meaning procfile parsing, and the environment variable is PIDUSAGE_USE_PS. The default is the better choice on Linux, where the proc files are the authoritative source and where ps is a presentation tool that samples and formats. Falling back to ps matters on systems where the proc filesystem is not there or is mounted differently, which is the normal case on macOS and on BSD.

The second is useGwmi, which is Windows only. When true it uses gwmi, which is the PowerShell module, directly, without checking whether wmic is available. The default is false, so the library probes for wmic first and falls back to the PowerShell path, which is the slower one.

That check exists because wmic was deprecated in Windows and is being removed. A library that shells out to wmic unconditionally would break on newer Windows releases, and one that hardcodes the PowerShell path would pay the slower route on every machine. The probe avoids both.

The third is maxage, the maximum age of a process on history, defaulting to 60000 milliseconds, with the environment variable PIDUSAGE_MAXAGE. This is about caching: a process's statistics do not change meaningfully within a short window, so a reading can be reused rather than recomputed. Sixty seconds is a long window for that, and the default is worth understanding before you rely on it, because it means a rapid poll can return the same figures more than once.

There is one more environment-only switch. PIDUSAGE_SILENT=1 removes every console message triggered by pidusage, which matters when the library is embedded in something whose output you do not want polluted.

The compatibility table is the honest part of the documentation

The compatibility table covers six properties across eight platforms, and it uses four symbols with a legend.

Linux, macOS and Alpine are marked as working for every property. Windows is working for memory, pid, ctime, elapsed and timestamp, but its cpu row is marked as not accurate. FreeBSD, NetBSD, SunOS and AIX are marked as should work.

The legend spells out the four states: working, not accurate, should work and not working. That last state is defined but does not appear in the table, which is a good sign about the current state of the ports.

The Windows CPU row is the one to pay attention to, and the reason is structural rather than a bug. The other platforms expose per-process CPU time, and the percentage is computed by dividing the CPU time consumed between two samples by the elapsed time. On Windows, the standard command line tooling does not give you that cumulative CPU time in a form you can subtract, so the reading has to be approximated from a share of total system CPU instead. It works, and it is not exact.

The three should work platforms are BSD, SunOS and AIX, where the implementation is expected to work but has not been confirmed by the test matrix. The test badges show what is actually covered: separate workflows for macOS, Linux, Windows and Alpine, plus a lint workflow, and a codecov badge for coverage. So the platforms with a workflow are the ones with real verification, and the should work entries are an informed expectation rather than a tested result.

The Alpine badge is worth noticing on its own. Alpine uses musl rather than glibc, so it is the platform most likely to break a process-inspection tool that shells out to standard utilities, and having a dedicated workflow for it is a deliberate choice.

The documentation closes the compatibility section by asking for an issue if your platform is not supported or if you have reported wrong readings, which is the right way to turn the should work entries into working ones.

The project structure and what it tells you

The repository layout is small and tells you how the implementation is organised.

There is index.js as the entry point, a lib/ directory with the implementation, and the package file lists exactly those two in its files array, so the published package contains the entry point and the library code and nothing else. There is a test/ directory, an examples/ directory, and a Dockerfile at the root.

The examples directory has three files. A README.md, a server.js, and a stresstest.js. The stress test is the interesting one to think about, because it is the tool you would use to check the overlap problem the documentation warns about, since a deliberately overloaded machine is exactly where the setInterval advice bites.

The development tooling is modern and small. The lint script runs standard, which is the JavaScript style guide as a tool, the test script runs ava with c8 for coverage and excludes the benchmark tests, and there is a separate coverage script that includes them and a bench script that runs only the benchmarks. The test runner is ava at version 6.4.1 and the coverage tool is c8, both current.

Two dev dependencies reveal the testing approach. mockdate is there because the readings depend on the current time, so the tests have to control the clock to assert on ctime, elapsed and timestamp. And mockery is there because the tests need to stub the process execution, which is how you test a library whose whole job is running commands and reading their output.

There is a related project, pidusage-tree, for computing a tree of process usage, which is the natural extension of this one since the API already accepts multiple PIDs and reports the parent PID of each.

The package also lists a contributor, Simone Primarosa, alongside the author Antoine Bluchet, and the CHANGELOG.md at the root records the release history. The project began as an implementation of the ideas in node-usage, and this one made the trade of pure JavaScript over native bindings so that it installs without a compiler.

Editorial conclusion

Adopt pidusage if you need to measure a process you do not control, since it reads the operating system's own process data and needs no native compilation. Skip it if you are measuring your own Node.js process, because process.cpuUsage and process.memoryUsage do that better with no dependency at all. Verify first which platform you are on and which fields you need, since the compatibility table marks the Windows CPU reading as not accurate, and that you poll on a timeout chain rather than setInterval, since the documentation warns that overlapping calls give wrong readings.

Frequently asked questions

What does pidusage report for a process?

It returns seven fields: cpu as a percentage from 0 to 100 times the number of cores, memory in bytes, ppid for the parent process ID, pid, ctime in milliseconds of user plus system time consumed, elapsed in milliseconds since the process started, and timestamp in milliseconds since the epoch. Passing an array of PIDs returns an object keyed by PID rather than a single stats object.

Why should I avoid setInterval when polling pidusage?

Because the calls could overlap with asynchronous processing. The measurement spawns a command and reads its output, and a setInterval fires regardless of whether the previous call has finished, so on a loaded machine several calls stack up and each reading reflects more than one sample. The documented fix is to chain setTimeout calls, scheduling the next one from the completion of the previous, and there are examples in both callback and async and await form.

Which platforms does pidusage support accurately?

Linux, macOS and Alpine are marked working for every property. Windows is working for memory, pid, ctime, elapsed and timestamp, but the cpu reading is marked not accurate. FreeBSD, NetBSD, SunOS and AIX are marked should work. The legend defines four states: working, not accurate, should work and not working, and the test workflows cover macOS, Linux, Windows and Alpine.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. soyuka/pidusage on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/soyuka-pidusage.svg)](https://hysenlabs.com/projects/soyuka-pidusage)

Was this useful?