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

prctl-subreaper

v0.1.1

Published

Make any Linux Node.js or Bun process a subreaper for its descendants. Calls prctl(PR_SET_CHILD_SUBREAPER, 1) plus runs a polling waitpid reaper for zombies whose ppid is the host process. Closes the libuv #1911 close-before-exit gap and the orphan-repare

Readme

prctl-subreaper

Make any Linux Node.js or Bun process a subreaper for its descendants — without tini, without a wrapper, without changing your application code.

npm install prctl-subreaper
NODE_OPTIONS="--require prctl-subreaper" node your-app.js

That's it.

What this fixes

Three classes of zombie processes that accumulate under any long-lived Node.js host:

| Path | Cause | Caught by tini-as-PID-1? | Caught by prctl-subreaper? | |---|---|---|---| | A. Orphan reparenting | Intermediate parent dies; child reparents to PID 1. | ✓ | ✓ | | B. libuv #1911 close-before-exit | Application calls uv_close() (or its Node equivalent) on a process handle before the spawned child exits. libuv unregisters its waitpid for that pid and never reaps. Child becomes a zombie attached to node directly — parent alive. | ✗ (parent's alive, not orphaned) | ✓ | | C. child.kill() leak | child.kill() sends SIGTERM but doesn't always result in waitpid being called, particularly under load. Same shape as B from the process tree. | ✗ if parent still alive, ✓ once parent dies | ✓ |

Path B is the under-discussed one. libuv's own current documentation reads, verbatim:

"On Unix, if the process has not yet exited when you call uv_close, you will create a zombie that libuv cannot reap. You are responsible for calling waitpid later."

The libuv source (src/unix/process.c, uv__wait_children) carries this comment:

"The child died, and we missed it. This probably means someone else stole the waitpid from us. Handle this by not handling it at all."

Path B is closed-not-fixed at the libuv layer. Tini-as-PID-1 — the canonical Docker fix for Node-in-containers — does not catch Path B because the zombie's parent is alive (it's node itself). This package catches all three paths in one place.

How it works

┌─────────────────────────────────────────────────────────────────────┐
│  on require:                                                       │
│    prctl(PR_SET_CHILD_SUBREAPER, 1)                                │
│      → orphans of any descendant now reparent to *us*, not PID 1.  │
│    spawn polling thread:                                           │
│      every 1 s, walk /proc/[pid]/status                            │
│      for each pid where State == Z and PPid == our pid             │
│        and (now - starttime) >= 5 s                                │
│      call waitpid(pid, &status, WNOHANG).                          │
└─────────────────────────────────────────────────────────────────────┘

The 5-second minimum age is the libuv co-existence trick. libuv reaps its tracked children (those it spawned via uv_spawn and still tracks) within milliseconds of SIGCHLD. By waiting 5 seconds before reaping, we let libuv reap-its-own first; we only catch the leaked tail. Configurable via PRCTL_SUBREAPER_MIN_AGE_MS.

We also use waitpid(specific_pid, WNOHANG), never waitpid(-1, ...). This eliminates the worst-case race: even if our reaper and libuv's handler fire simultaneously on the same pid, exactly one wins; the loser gets ECHILD and (per the comment above) "handles this by not handling it at all." If we lose, libuv handles it. If libuv loses, we handle it. Either way the zombie is reaped.

Loading the package

Auto-load via NODE_OPTIONS

For an existing application you don't want to modify:

NODE_OPTIONS="--require prctl-subreaper" node app.js

In a systemd unit file

[Service]
Environment="NODE_OPTIONS=--require prctl-subreaper"
Environment="NODE_PATH=/usr/local/lib/node_modules"
ExecStart=/usr/local/bin/node /path/to/app.js

Programmatically

require('prctl-subreaper'); // auto-init on require
// optional:
const sub = require('prctl-subreaper');
console.log(sub.stats()); // { running: true, pid, intervalMs, minAgeMs, reapedCount }

Bun

BUN_OPTIONS="--preload prctl-subreaper" bun run app.ts

(Bun's --preload is the analog of Node's --require. Bun is also dynamically linked and the addon's prctl call works the same way under Bun's runtime.)

Configuration

All via environment variables. Read once at init() time.

| Variable | Default | Range | Purpose | |---|---|---|---| | PRCTL_SUBREAPER_INTERVAL_MS | 1000 | 50 – 600000 | Polling interval for the reaper thread. Lower = faster reaping, higher CPU. 1 s is fine for most workloads. | | PRCTL_SUBREAPER_MIN_AGE_MS | 5000 | 0 – 600000 | Zombies younger than this are skipped — gives libuv time to reap its own tracked children first. Set to 0 to reap immediately (only safe if you don't care about libuv child.on('exit') callbacks). | | PRCTL_SUBREAPER_SILENT | unset | 0/1 | If 1, suppress the warning log when the native addon fails to load. |

API

// Auto-called on require.  Idempotent.  Returns true on success.
function init(): boolean;

// Stop the polling thread (does NOT clear PR_SET_CHILD_SUBREAPER — there is
// no API to clear it).  Mostly for tests.
function stop(): boolean;

// Snapshot of runtime state.
function stats(): {
  running: boolean;
  pid: number;
  intervalMs: number;
  minAgeMs: number;
  reapedCount: bigint;
};

// True iff this build supports prctl(PR_SET_CHILD_SUBREAPER) — i.e., Linux
// with the native addon successfully loaded.
function isSupported(): boolean;

Verifying it works

The most direct test is to spawn-and-leak a process, then count zombies before and after:

// without prctl-subreaper:
const { spawn } = require('child_process');
const c = spawn('sleep', ['0.1']);
c.unref();
// ... if uv_close runs before child exits, zombie persists.

// with prctl-subreaper loaded:
require('prctl-subreaper');
// same code → zombie is reaped within PRCTL_SUBREAPER_MIN_AGE_MS + interval.

Inspect zombie state with:

ps axo pid,ppid,stat,comm | awk '$3 ~ /Z/'

After 5–6 seconds (default min age + one polling cycle) the row should disappear.

Platform support

| Platform | Status | |---|---| | Linux x64 / arm64 | Supported (native addon). Kernel 3.4+ for PR_SET_CHILD_SUBREAPER. | | macOS | No-op. isSupported() returns false; init does nothing. Won't crash your app. | | Windows | No-op. Same as macOS. | | Linux musl (Alpine) | Supported. N-API ABI is libc-independent. |

Bun status (verified 2026-05-05): The same .node binary built against Node 22.22.2's N-API loads cleanly under Bun 1.3.13 via Bun's N-API compatibility layer. Both bun -e "require('prctl-subreaper')" and bun --preload prctl-subreaper work — stats() returns { running: true, supported: true } and the reaper thread is alive in the Bun process.

The package's os field in package.json is ["linux"] so npm warns on non-Linux installs, but the JS layer also short-circuits at runtime so cross-platform projects keep working.

Race semantics with libuv (in detail)

Read this if you maintain a Node application that relies on child_process exit callbacks and want to know whether this package can break them.

libuv installs a process-wide SIGCHLD handler. When SIGCHLD arrives, libuv iterates its internal queue of tracked children (handles spawned via uv_spawn and not yet uv_close'd) and calls waitpid(specific_pid, WNOHANG) on each. When a known child has exited, libuv's exit callback fires the user's child.on('exit', ...) event.

prctl-subreaper runs on a separate kernel thread. Every PRCTL_SUBREAPER_INTERVAL_MS, it walks /proc and reaps qualifying zombies via waitpid(specific_pid, WNOHANG). Three race outcomes are possible:

  1. libuv reaps first: by the time our reaper visits the zombie's pid, the kernel has already cleaned it up. Our waitpid returns -1 with errno=ECHILD. We continue. libuv has fired the user's on('exit') correctly. No bug.

  2. Our reaper reaps first AND libuv was tracking: we steal libuv's waitpid. When libuv's SIGCHLD handler runs, libuv's waitpid(specific_pid) returns -1/ECHILD, libuv treats it as "handled by not handling it at all." The user's on('exit') callback does not fire. This is the failure mode we work to avoid. The 5-second minimum-age threshold (PRCTL_SUBREAPER_MIN_AGE_MS) is the avoidance mechanism: libuv reaps its own children within milliseconds of SIGCHLD. By waiting 5 s, we let libuv finish first.

  3. Our reaper reaps first AND libuv was NOT tracking (the libuv #1911 case, or post-uv_close): libuv has no tracking entry for this pid; its SIGCHLD handler ignores the dead child. Our waitpid succeeds. No callback was scheduled — there's nothing to break. This is the case we exist to handle.

If your application has a hard requirement that every child.on('exit') callback fire and you cannot tolerate the failure mode in case 2, raise PRCTL_SUBREAPER_MIN_AGE_MS higher (e.g., 30 s) until you observe no missing-exit-callback regressions in your test suite.

Why we don't use waitpid(-1, WNOHANG)

A naive subreaper would use waitpid(-1, WNOHANG) in a SIGCHLD handler — which is the textbook approach for an init process. We deliberately do not, because installing a process-wide SIGCHLD handler would replace libuv's, breaking all of Node's child-process tracking. Polling /proc and using per-pid waitpid is slower but coexists with libuv cleanly.

Comparison with alternatives

| Tool | Approach | Pros | Cons | |---|---|---|---| | tini (PID 1 in Docker) | Tiny init, replaces PID 1 of the container. | Battle-tested. ~30 KB binary. | Requires a wrapper; doesn't catch Path B (libuv close-before-exit) zombies. | | dumb-init (PID 1 in Docker) | Same as tini, different impl. | Same as tini. | Same as tini. | | Shopify/child_subreaper (Ruby) | prctl(PR_SET_CHILD_SUBREAPER, 1) in Ruby. | Same idea as this package. ~10 lines. | Ruby-only. | | rootmos/dont-fear-the-reaper (Rust) | prctl + walk /proc + send SIGTERM (terminates orphans). | Standalone CLI. | Terminates rather than reaps; experimental (4 stars). | | prctl-subreaper (this) | prctl + polling waitpid in a separate thread, libuv-aware. | Single-package fix; no wrapper; auto-init via --require; works in Node and Bun. | Linux only. |

Prior art and references

License

MIT. See LICENSE.

Contributing

PRs welcome. The C++ source is a single file (src/addon.cc); the JS layer is a single file (lib/index.js). Open an issue first if you want to add a feature that changes the public API.

This package's design assumes Linux. Maintainers will not accept PRs that add platform-specific implementations for non-Linux subreaper equivalents — that scope belongs in a separate package.