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

pm2-release

v0.2.1

Published

Health-gated deploys and rollback for PM2. Rolling releases that stop and revert when the new code fails its probes.

Readme

pm2-release

npm CI licence: MIT node

Health-gated deploys and rollback for PM2. Rolls a release out one instance at a time, checks each one before touching the next, and puts the previous release back when the new code fails.

pm2 reload restarts every instance and reports success if the processes came back. It has no idea whether the new code works, and by the time you find out, the whole fleet is running it. pm2-release keeps the rest of the fleet on the old release while the new one is judged, so a bad deploy costs one instance instead of all of them.

npm install -g pm2-release
pm2-release deploy api --from ./build --root /srv/api

See it work

A real two-instance cluster app, captured from a live PM2 daemon. The first release is good; the second serves 503 from its health endpoint.

$ pm2-release deploy api --from ./build-good --root /srv/api --json
{"ok":true,"releaseId":"20261103-024537-3hoym9","previousId":"20261103-024532-p7ha56",
 "rolledBack":false,"rollout":{"ok":true,"reloaded":[0,1]},"pruned":[]}

$ pm2-release deploy api --from ./build-bad --root /srv/api --json
{"ok":false,"releaseId":"20261103-024600-u314us","previousId":"20261103-024537-3hoym9",
 "rolledBack":true,
 "rollout":{"ok":false,"reloaded":[],"failedAt":0,
            "reason":"instance 0 did not become healthy: status 503 (via http)"},
 "reason":"instance 0 did not become healthy: status 503 (via http)"}

$ echo $?
1

"failedAt":0 with "reloaded":[] is the point: it stopped at the first instance. Instance 1 never ran the bad code, and current went back to the release that works. The non-zero exit is what makes CI go red without extra plumbing.

$ pm2-release list api --root /srv/api
  20261103-024600-u314us  2026-11-02T21:47:00.545Z  (rejected)
* 20261103-024537-3hoym9  2026-11-02T21:45:37.465Z
  20261103-024532-p7ha56  2026-11-02T21:45:32.012Z

How a release is laid out

The Capistrano shape, which PM2 supports by accident and reliably:

/srv/api/
  releases/
    20261103-020659-dqwws0/      the code
    20261103-021037-x0ar6q/      an older release, kept for rollback
  current -> releases/20261103-020659-dqwws0

Start your app through the symlink, once:

// ecosystem.config.js
module.exports = { apps: [{
  name: 'api',
  script: '/srv/api/current/app.js',
  cwd: '/srv/api/current',
  instances: 2,
  exec_mode: 'cluster',
  increment_var: 'PORT',        // see "Cluster mode needs per-instance ports"
  env: { PORT: 3000 },
}] };

This works because PM2 stores the symlink in pm_exec_path and pm_cwd rather than resolving it, so a reload picks up whatever current points at now. That was verified against PM2 7.1.1 rather than assumed; it is finding F16 in pm2-ops-contract.

Seeding an empty root

The first deploy into an empty root has nothing to reload, so it reports failure with no running instances. That is expected: it still writes the release and points current at it, which is what you then start the app from.

pm2-release deploy api --from ./build --root /srv/api   # writes the release, exits 1
pm2 start ecosystem.config.js                           # starts through /srv/api/current
pm2-release deploy api --from ./build --root /srv/api   # gated from here on

A release the gate never got to judge is not marked as rejected, so it stays a valid rollback target.

A deploy is then: write the new release, swap the symlink, reload one instance, check it, repeat. A rollback is the same thing pointed at an older directory.


Getting the code onto the box

Two sources. The directory is the one to use.

# whatever your CI already produces: rsync, scp, an artifact download, a container copy
pm2-release deploy api --from ./build --root /srv/api

# or let it clone, for setups with no artifact pipeline
pm2-release deploy api --git [email protected]:you/api.git --ref main --install --root /srv/api

Git mode does a shallow clone, optionally runs npm ci --omit=dev (falling back to npm install with no lockfile), and appends the short commit sha to the release id so a running release is traceable to a commit. It then deletes .git from the release: a deployed release is not a working copy, and .git/config holds the remote URL, which in CI routinely carries a token.


The health gate

Three sources, tried in order, first usable one wins:

| Source | What it is | When it applies | | --- | --- | --- | | probe | pm2-liveness's own verdict, over PM2's bus | that module is installed and has judged this instance | | http | GET /health on the instance's own port | the app has a per-instance PORT | | uptime | came back, stayed up, did not crash again | always; weak, and honest about it |

probe is first on purpose. pm2-liveness already runs the real probes with the operator's own thresholds, and two health opinions disagreeing mid-deploy is the worst possible time for it. If that module is absent, nothing arrives on the bus and the gate falls through.

Each instance must stay healthy for soakSeconds before the next one is touched. An instance that never gets there inside waitSeconds fails the release.

What the uptime gate can and cannot tell you

It catches a release that crashes on boot or crash-loops. It cannot catch a release that starts cleanly and serves garbage. If you have no pm2-liveness and no health endpoint, that is the guarantee you are getting, and the README is not going to pretend otherwise.

--no-gate goes further and skips health entirely: it waits only for the process to come back. That is a deploy, not a gated release.

A note on one subtlety, because it caused a bug: PM2 increments restart_time by exactly one on a reload, in both cluster and fork mode. The uptime gate therefore compares against "the count we expect once our own reload lands", not the count from before it. Comparing against the earlier number reports restarted 1 time(s) on every successful deploy, which made --no-gate roll every release back.


Cluster mode needs per-instance ports

The http gate asks a specific instance whether it is healthy. In cluster mode every instance listens on the same port by default, so the request is answered by whichever one the kernel picks, and the answer says nothing about the instance just reloaded.

increment_var: 'PORT' gives each instance its own port and makes the check meaningful. Without it, pm2-release skips the http source rather than trusting a result it knows is meaningless, and falls through to uptime.


Commands

pm2-release deploy <app> --from <dir>
pm2-release deploy <app> --git <url> [--ref main] [--install]
pm2-release rollback <app> [--to <release-id>]
pm2-release list <app>

| Flag | Meaning | | --- | --- | | --root <dir> | where releases/ and current live (required unless configured) | | --config <file> | YAML or JSON file with per-app defaults | | --no-gate | reload without waiting for health | | --no-rollback | stop on failure instead of reverting | | --keep <n> | releases to retain besides the current one (default 5) | | --json | machine-readable result on stdout |

Rollback picks the release that was actually running

rollback with no --to does not mean "the newest directory that is not current". A deploy that fails its gate leaves its directory behind, so that rule rolls you forward onto the exact code you just refused to run. Rejected releases are tombstoned on disk and are never a rollback target; the default target is the predecessor recorded by the last successful deploy. A second rollback walks further back rather than bouncing between the same two releases.

A tombstoned release is still reachable by name with --to, and is the first thing pruned.


Configuration file

Everything can come from flags. A file is for when you have more than one app.

# /etc/pm2-release.yaml
logLevel: info

apps:
  api:
    root: /srv/api
    keep: 5
    rollbackOnFailure: true
    gate:
      order: [probe, http, uptime]
      path: /health
      host: 127.0.0.1
      expectStatus: [200, 399]
      timeoutMs: 3000
      soakSeconds: 10      # healthy for this long before moving to the next instance
      waitSeconds: 60      # give up on an instance after this long

watch:
  enabled: false
  soakMinutes: 15
  autoRollback: false
  failuresBeforeRollback: 3
  intervalSeconds: 10

An invalid file is rejected with the offending path named, rather than being silently replaced by defaults.


The watcher (optional)

A gate that passes proves the new code starts and answers. It does not prove the code survives real traffic, a slow leak, or a dependency that only fails under load.

Installed as a PM2 module, pm2-release runs a small resident process that keeps watching a release for soakMinutes after it goes live and can put the previous one back.

pm2 install pm2-release
pm2 set pm2-release:configFile /etc/pm2-release.yaml
pm2 set pm2-release:watchEnabled true

It uses the same health sources as the gate, and needs failuresBeforeRollback consecutive bad checks before it acts. One refused connection during a GC pause is not a reason to revert production.

autoRollback is off by default. A deploy reverting itself unattended, minutes later, is a surprise in the middle of someone else's incident unless they asked for it. Left off, the watcher logs the degradation and does nothing else.

If the app has neither pm2-liveness nor a per-instance port, the watcher says so once and stays quiet, because there is nothing it could detect.

The config file is re-read in place, so changing autoRollback during an incident does not mean restarting the module. It is polled by inode, mtime and size rather than watched with fs.watch: an editor or sed -i replaces the inode, and fs.watch follows the old one and is permanently dead after the first edit. That was verified across every module in this suite.


Events

Nothing is published yet. The deploy marker at <root>/.pm2-release-state.json records what is running, what preceded it, and when, and is the contract between the CLI and the watcher. The two halves are deliberately decoupled: the CLI has to work with no watcher running, which is the normal CI case.


Use with the rest of the suite

| Module | Relationship | | --- | --- | | pm2-liveness | supplies the strongest health gate; install it first if you can | | pm2-alertmanager | pages a human when a release degrades after deploy | | pm2-shipper | keeps the logs of a release that was rolled back |

None of them are required. pm2-release works on its own, with a weaker gate.


Troubleshooting

Every pm2 command hangs and never returns

PM2_HOME is too long. PM2 binds $PM2_HOME/rpc.sock, and Linux caps unix socket paths at 107 characters. Over that the bind fails silently: no socket is created, every command blocks forever, and the orphaned daemon spins at 100% CPU.

echo -n "$PM2_HOME/rpc.sock" | wc -c   # must be < 107

The deploy fails with no usable health source

No pm2-liveness verdict, no per-instance port, and uptime is not in gate.order. Add uptime to the order, add increment_var: 'PORT', or install pm2-liveness.

A crash-looping release takes the full waitSeconds to fail

It does, by design. An app that crashes once on boot while it waits for a dependency and then comes up fine is a real pattern, so one extra restart is not treated as fatal. Lower waitSeconds if you would rather fail fast.

The app still runs the old code after a deploy

It was not started through the symlink. Check pm2 jlist for pm_exec_path: it must contain /current/, not a resolved release directory. PM2 keeps whatever path it was given at start time, so an app started directly against a release directory stays pinned to it forever.


Design documentation

  • docs/requirements.md - what this module was built against, including the requirements that were revised once the implementation met a live daemon.
  • Platform findings F16 and F17, which this module depends on, are in pm2-ops-contract.

Requirements

Node 22 or newer - tested on 22, 24 and 26 - with PM2 5 or newer.

Development

npm install
npm run build
npm test

The end-to-end behaviour is exercised by scenario 60 in the suite lab (pm2-ops-contract/lab), which runs a real two-instance cluster app against a real PM2 daemon, deploys a good release and a bad one, and asserts that the bad one is caught at the first instance and reverted.

The rest of the suite

pm2-release is one of five independent PM2 modules. They install, version and run separately - none depends on another - but they are built against one shared set of verified PM2 platform findings, so they behave consistently and do not fight each other.

| Module | What it does | | --- | --- | | pm2-liveness | real health probes and a verdict on the bus | | pm2-alertmanager | crash and probe alerts, deduplicated and routed | | pm2-shipper | log shipping that survives rotation | | pm2-secrets | secrets fetched from a vault and injected at restart |

pm2-liveness publishes a verdict on the PM2 bus that pm2-alertmanager and pm2-release both consume, which is the one place the modules genuinely talk to each other.

License

MIT