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.

Verdict

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 Node or npm just to supervise a Python or Go service.
  • You want every restart as a queryable run row, not a tail log.
  • Scheduled jobs deserve their own kind, not cron_restart.
  • You want a Web UI in the binary, not a hosted dashboard on a paid plan.

Stay on PM2 if

  • Your app needs Node's shared-socket cluster mode today.
  • max_memory_restart, watch mode or pm2 deploy are part of your workflow.
  • pm2 reload rolling restarts are load-bearing for deploys.
  • Native Windows hosting is non-negotiable.
  • pm2.io's hosted dashboard with retained metrics is in use.
Head to head

Yes / no, at a glance

Yes / no — and both columns win on something.

Capability PM2 RunWisp
Runtime-free single binary ✗needs Node or Bun ✓
Runs non-Node programs ✓interpreter: option ✓
Declarative config ~JS / JSON / YAML ✓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, 1.5x ✓constant / linear / exp
Give up after repeated fast-fails ✓min_uptime + max_restarts → errored ✓restart_attempts → FATAL
Health-check command ✗ ✓health_check
Per-run history rows ✗restart counter only ✓
Web UI in the binary ✗pm2.io is hosted ✓
TUI ✓pm2 monit ✓
Live CPU / memory per process ✓pm2 monit ✓
Stops the whole process tree ~walks the PID tree, SIGINT by default ✓process group, SIGTERM
Coalesced failure notifications ✗ ✓
Restart on memory limit ✓max_memory_restart ✗
Zero-downtime rolling reload ✓pm2 reload ✗
Auto-write init script ✓pm2 startup ✓runwisp service install
Native Windows ✓ ✗Linux/macOS/WSL
Trade-offs

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.
  • max_memory_restart restarts a leaking worker; watch mode restarts on file change; pm2 deploy ships code.
  • 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 plans).

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.
  • Every replica start and exit is a row in the Web UI with exit code and captured streams.
  • instances = N spawns N independent crash domains, each with RUNWISP_INSTANCE_INDEX, not a master fan-out.
  • [tasks.*] and [services.*] split at the schema; cron_restart isn't a substitute.
  • Restart backoff you pick, restart_attempts before a FATAL stop, and an optional health_check command.
  • Stop signals the whole process group, then SIGKILLs stragglers after the grace period.
  • Web UI and TUI ship in the binary: no hosted account, no license key.
The simple case

The simple case

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

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

The version that runs in prod

Three workers, hardened. cluster mode + max_memory_restart on the left; independent crash domains and named backoff on the right. PM2's memory cap has no RunWisp key.

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,      // SIGINT grace (default signal), 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, three workers, production
[services.api-worker]
description      = "Three Node workers consuming the same job queue"
run              = "node ./worker.js"
instances        = 3                # each gets RUNWISP_INSTANCE_INDEX
restart_delay    = "1s"
restart_backoff  = "exponential"
healthy_after    = "10s"            # uptime before a start counts as stable
restart_attempts = 10               # fast-fail this many times, then go FATAL
graceful_stop    = "20s"            # SIGTERM, then SIGKILL; keep [daemon] shutdown_timeout above this
env              = { NODE_ENV = "production", QUEUE_URL = "redis://localhost:6379" }
keep_runs        = 500
notify           = ["slack-ops"]
# no max_memory_restart equivalent: cap memory in the unit or container
Migration

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 healthy_after
max_restarts restart_attempts (then FATAL)
exp_backoff_restart_delay restart_backoff = "exponential"
kill_timeout: 20000 (SIGINT first) graceful_stop = "20s" (SIGTERM first; set stop_signal = "SIGINT" to match)
max_memory_restart: "512M" No equivalent. Cap memory with a cgroup (systemd, Docker); --max-old-space-size aborts on OOM instead of restarting cleanly.
env_production: { … } env = { … }
cron_restart Model as [tasks.*] with cron =
pm2 logs runwisp logs -f <service>, the Web UI, or runwisp tui
pm2 reload (rolling restart) No zero-downtime rolling restart; runwisp reload re-reads config in place
pm2 startup + pm2 save runwisp service install (systemd / launchd); the config is the saved state
Gotchas

Gotchas

instances is not Node cluster mode

instances = N spawns N independent processes. No master, no shared listening socket; each process gets only RUNWISP_INSTANCE_INDEX. Code that branches on cluster.isPrimary won't recognise the topology. Bind with SO_REUSEPORT inside workers or front them with a reverse proxy.

No zero-downtime rolling reload

PM2's pm2 reload cycles workers one at a time so the listening port never drops; RunWisp has no equivalent. runwisp reload (or SIGHUP) re-reads runwisp.toml in place, validate-first, but a restarted service does drop its connections. If rolling deploys are load-bearing, keep PM2 or front the workers with a proxy.

No max_memory_restart

PM2 restarts a worker whose RSS passes max_memory_restart. RunWisp has no memory cap key. Use a cgroup limit (a systemd unit, docker run --memory), which kills the process so RunWisp restarts it. node --max-old-space-size is a V8 heap limit that aborts on OOM, so it isn't the same thing.

runwisp service install is the pm2 startup equivalent

Like PM2's pm2 startup, runwisp service install writes and enables the init unit for you: systemd or launchd, with companions service uninstall / service status and --print to emit the unit without installing. In a container, use the entrypoint instead.

FAQ

FAQ

Can RunWisp actually run my Node app?
Yes. run = "node app.js" or whatever you would type at a shell. RunWisp spawns the command, captures stdout and stderr, watches the exit code, and writes a run row. The TOML is identical whether run is Node, Python, Go, or sh.
Does RunWisp do cluster mode like PM2?
Not in the node:cluster sense. instances = N runs N independent processes. They don't share a listening socket and there is no master. If you need a shared port, bind with SO_REUSEPORT (the reusePort option on recent Node) or front the workers with a reverse proxy.
How do I replace cron_restart?
There is no "restart this service at 04:00" key. A service restarts after any exit, so a worker that exits on its own after N jobs or hours comes straight back. If the real need is a scheduled job, model it as a [tasks.*] with cron = and on_overlap = "skip".
Can I run PM2 and RunWisp side by side during migration?
Yes. They don't share state, sockets, or log paths unless you point them at the same files. Move one ecosystem.config.js entry at a time; roll back by reversing the same two steps.
Is there a pm2 monit equivalent?
Yes, twice: an embedded Web UI served by the daemon and runwisp tui, both showing run history, live logs, and live CPU and memory per task and service. Both ship in the binary, with no separate process and no hosted account.
Does PM2 kill child processes on stop?
Mostly. PM2 walks the process tree and signals every descendant (SIGINT by default), then SIGKILLs survivors after kill_timeout. Children that were reparented or double-forked can escape it. RunWisp signals the whole process group with SIGTERM and kills what's left after graceful_stop.
Get started
GPL 3.0 · no accounts · free to self-host

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

Copy an ecosystem.config.js app into a services stanza, set instances, and run runwisp validate. Every restart lands in run history with its exit code and output.

Install in 30 seconds
curl -fsSL https://get.runwisp.com | sh