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.
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,watchmode orpm2 deployare part of your workflow.pm2 reloadrolling restarts are load-bearing for deploys.- Native Windows hosting is non-negotiable.
- pm2.io's hosted dashboard with retained metrics is in use.
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 |
Pros & cons
What PM2 still does better
- Node
clustermode with a shared listening socket and master round-robin. pm2 reloadrolls workers one at a time; the listening port never drops.max_memory_restartrestarts a leaking worker;watchmode restarts on file change;pm2 deployships code.npm i -g pm2in 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 byrunwisp validate. - Every replica start and exit is a row in the Web UI with exit code and captured streams.
instances = Nspawns N independent crash domains, each withRUNWISP_INSTANCE_INDEX, not a master fan-out.[tasks.*]and[services.*]split at the schema;cron_restartisn't a substitute.- Restart backoff you pick,
restart_attemptsbefore a FATAL stop, and an optionalhealth_checkcommand. - 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
Keep one Node API process alive. Four JS lines vs one TOML stanza.
// ecosystem.config.js
module.exports = {
apps: [
{
name: "api",
script: "/srv/api/server.js",
},
],
}; # runwisp.toml
[services.api]
run = "node /srv/api/server.js" 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, 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, 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 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
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
Can RunWisp actually run my Node app?
Does RunWisp do cluster mode like PM2?
How do I replace cron_restart?
Can I run PM2 and RunWisp side by side during migration?
Is there a pm2 monit equivalent?
Does PM2 kill child processes on stop?
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.
curl -fsSL https://get.runwisp.com | sh