RunWisp

Comparison · vs PM2

RunWisp vs PM2: process supervision without the Node runtime.

A real ecosystem.config.js next to the equivalent runwisp.toml service. Replicas without Node cluster mode, restart backoff curves, scheduled jobs as their own kind, every restart as a row in history.

Quick start GitHub

Should you switch?

The 10-second version. Skim, decide, scroll down for the receipts.

Switch to RunWisp if

  • Your fleet isn't all Node; you'd rather not ship npm everywhere.
  • You want every restart as a queryable run row, not a tail log.
  • Scheduled jobs deserve their own kind, not cron_restart.
  • Restart backoff should curve, not cap-and-stop.
  • Graceful stop should kill the worker's children too, not just the parent.
  • A Web UI / TUI without a paid hosted tier matters.

Stay on PM2 if

  • Your app needs Node's shared-socket cluster mode today.
  • pm2 reload rolling restarts are load-bearing for deploys.
  • pm2 startup writing an init script is real ergonomic win.
  • Native Windows hosting is non-negotiable.
  • pm2.io's hosted dashboard with retained metrics is in use.

Yes / no, at a glance

Yes / no, both columns win on something.

Feature PM2 RunWisp
Runtime-free single binary
needs Node
Language-agnostic supervisor
~ Node-flavoured
Declarative config
JS module
validated TOML
Always-on services
Scheduled jobs
~ cron_restart
[tasks.*]
Replicas as one config
Node cluster shared socket
Restart-backoff curve
~ exp_backoff_restart_delay
Duration-based stability reset
max_restarts cap
backoff_reset_after
Per-run history rows
Web UI in the binary
pm2.io is hosted
TUI
pm2 monit
Process-group-wide graceful stop
Coalesced failure notifications
Zero-downtime rolling reload
pm2 reload
Auto-write init script
pm2 startup
Native Windows
Linux/macOS/WSL

Pros & cons

What PM2 still does better

  • Node cluster mode with a shared listening socket and master round-robin.
  • pm2 reload rolls workers one at a time; the listening port never drops.
  • pm2 startup writes systemd / launchd / init units for you.
  • npm i -g pm2 in a pure-Node shop is the smaller artifact.
  • Native Windows hosting today.
  • pm2.io's hosted dashboard with retained metrics ships now (paid tier).

What RunWisp adds

  • Single static binary; run = "node app.js" works without Node on the host.
  • Declarative runwisp.toml validated up-front by runwisp validate.
  • instances = N spawns N independent crash domains, not a master fan-out.
  • [tasks.*] and [services.*] split at the schema; cron_restart isn't a substitute.
  • Restart backoff as a curve plus backoff_reset_after; no terminal "errored" state.
  • Every replica start and exit is a row in the Web UI with exit code and captured streams.
  • Graceful stop sends SIGTERM to the whole process group by default.
  • Web UI and runwisp tui ship in the binary; no paid tier gates.

The simple case

Keep one Node API process alive. Four JS lines vs one TOML stanza.

ecosystem.config.js
ecosystem.config.js
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name:   "api",
      script: "/srv/api/server.js",
    },
  ],
};
runwisp.toml
runwisp.toml
# runwisp.toml
[services.api]
run = "node /srv/api/server.js"

Production-grade

Three workers, hardened. cluster mode + max_memory_restart on the left; independent crash domains and named backoff on the right.

ecosystem.config.js (production)
ecosystem.config.js (production)
// ecosystem.config.js, three workers, production
module.exports = {
  apps: [
    {
      name:          "api-worker",
      script:        "./worker.js",
      instances:     3,
      exec_mode:     "cluster",
      min_uptime:    "10s",
      max_restarts:  10,
      exp_backoff_restart_delay: 100,        // ms
      max_memory_restart:        "512M",
      kill_timeout:              20000,      // SIGTERM grace, ms
      out_file:   "/var/log/api-worker.out.log",
      error_file: "/var/log/api-worker.err.log",
      env_production: {
        NODE_ENV:  "production",
        QUEUE_URL: "redis://localhost:6379",
      },
    },
  ],
};
runwisp.toml (production)
runwisp.toml (production)
# runwisp.toml, three workers, production
[services.api-worker]
description         = "Three Node workers consuming the same job queue"
instances           = 3
restart_delay       = "1s"
restart_backoff     = "exponential"
backoff_reset_after = "2m"      # how long "up" counts as stable
graceful_stop       = "20s"     # finish the in-flight job before SIGKILL
env                 = { NODE_ENV = "production", QUEUE_URL = "redis://localhost:6379" }
keep_runs           = 500
notify_on_failure   = ["slack-ops"]
run = """
trap 'echo "SIGTERM received, draining"; exit 0' TERM INT
node --max-old-space-size=512 ./worker.js
"""

Migration cheat sheet

ecosystem.config.js field on the left, runwisp.toml field on the right.

PM2 RunWisp
apps[].name [services.<name>] or [tasks.<name>]
script: run =
instances: 3 instances = 3
exec_mode: "cluster" N independent processes; bind with SO_REUSEPORT or proxy in front
min_uptime + max_restarts backoff_reset_after + restart_backoff
exp_backoff_restart_delay restart_backoff = "exponential"
kill_timeout: 20000 graceful_stop = "20s"
max_memory_restart: "512M" node --max-old-space-size=512 inside run
env_production: { … } env = { … }
cron_restart Model as [tasks.*] with cron =
pm2 logs Per-run log download from Web UI; runwisp tui
pm2 reload runwisp validate + daemon restart
pm2 startup Write a systemd / launchd unit yourself (docs/operations/autostart)

Gotchas

FAQ

More comparisons: RunWisp vs cron · RunWisp vs supervisord · RunWisp vs systemd timers .

One binary supervising your processes. No Node runtime on the host.

Copy an ecosystem.config.js app into a services stanza, set instances, restart the daemon. Replicas come up indexed and visible, with every restart in run history.