Comparison · vs supervisord
RunWisp vs supervisord: process supervision without Python.
A real supervisord.conf [program:*] block next to the equivalent runwisp.toml service. Replicas, restart backoff, graceful stop, reload semantics.
Should you switch?
The 10-second version. Skim, decide, scroll down for the receipts.
Switch to RunWisp if
- You'd rather not ship Python on every supervised box.
- You want every restart as a queryable run row, not a log tail.
- Replicas should be one config, not N templated programs.
- Restart backoff needs a curve and a duration-based reset.
- You also have scheduled jobs that don't fit a [program:*] mould.
- Graceful-stop default should kill the process group, not just the parent.
Stay on supervisord if
-
supervisorctl reread + updateon one program matters. - You lean on the event-listener fan-out for lifecycle hooks.
- Existing tooling speaks the XML-RPC API today.
- Python's already on the box; one fewer artifact to ship.
Yes / no, at a glance
Yes / no, both columns win on something.
| Feature | supervisord | RunWisp |
|---|---|---|
| Runtime-free single binary | needs Python | |
| Always-on services | | |
| Scheduled jobs (cron-shaped) | | [tasks.*] |
| Replicas in one config | ~ numprocs templating | instances = N |
| Restart-backoff curve | startretries cap only | |
| Duration-based stability reset | | backoff_reset_after |
| Per-run history rows | | |
| Web UI | ~ built-in HTTP | |
| TUI | | |
| Graceful stop kills process group by default | ~ opt-in | |
| Coalesced failure notifications | | |
| Surgical per-program reload | | daemon-wide |
| Event-listener fan-out | | |
| Documented RPC API | XML-RPC | ~ REST |
| Per-program user identity | | ~ sudo wrapper |
| Bounded daemon shutdown | stopwaitsecs serial | |
Pros & cons
What supervisord still does better
-
supervisorctl reread + updatereloads one program without touching the rest. - Documented event-listener subprocess protocol for lifecycle side effects.
- Long tail of XML-RPC clients in every language.
- Decades of Puppet, Ansible, and SaltStack modules assume it exists.
What RunWisp adds
- One static Go binary; no Python, no venv, no pip.
-
instances = Nas one service with unified per-service logs. - Restart backoff as a curve (constant / linear / exponential) plus
backoff_reset_after. - Every replica start and exit is a row in the Web UI with exit code and captured streams.
- Tasks (
cron,on_overlap, retries) split from services at the schema level. - Process-group-wide graceful stop is the default.
- Slack / Telegram / email notifiers coalesce flapping into one alert plus a summary.
- Daemon shutdown fans grace windows out in parallel under a single ceiling.
The simple case
Keep one API process alive. Four INI lines vs one TOML stanza.
; /etc/supervisor/conf.d/api.conf
[program:api]
command = /usr/local/bin/api
user = api
autostart = true
autorestart = true
stdout_logfile = /var/log/api.log
redirect_stderr = true # runwisp.toml
[services.api]
run = "/usr/local/bin/api" Production-grade
Three replicas of a queue worker. Templated programs on the left, one service with instances = 3 on the right.
; /etc/supervisor/conf.d/api-worker.conf, three replicas, production
[program:api-worker]
command = /usr/local/bin/worker
process_name = %(program_name)s_%(process_num)02d
numprocs = 3
user = api
autostart = true
autorestart = true
startsecs = 5 ; uptime before considered "successfully started"
startretries = 5 ; give up after this many start failures
stopsignal = TERM
stopwaitsecs = 20 ; finish the in-flight job
stdout_logfile = /var/log/api-worker_%(process_num)02d.log
redirect_stderr = true # runwisp.toml, three replicas, production
[services.api-worker]
description = "Three always-on workers consuming the same job queue"
instances = 3
restart_delay = "2s"
restart_backoff = "exponential"
backoff_reset_after = "2m" # this one needs longer to call "stable"
graceful_stop = "20s" # leave time to finish the in-flight job
keep_runs = 500
notify_on_failure = ["slack-ops"]
run = """
trap 'echo "SIGTERM received, draining and exiting"; exit 0' TERM INT
echo "[$(date -Iseconds)] worker starting up..."
while true; do
/usr/local/bin/consume-job
done
""" Migration cheat sheet
supervisord field on the left, runwisp.toml field on the right.
| supervisord | RunWisp |
|---|---|
[program:api] | [services.api] (always-on) or [tasks.api] |
command = | run = |
numprocs = 3 | instances = 3 |
%(process_num)s | No per-replica index; declare N services if they need to differ |
autorestart = true | Implicit; services are always-on |
startsecs / startretries | backoff_reset_after + restart_backoff |
stopwaitsecs = 20 | graceful_stop = "20s" |
stopasgroup / killasgroup | Default (process-group-wide) |
stdout_logfile = … | Captured per run; download from Web UI |
user = api | Wrap run with sudo -u |
supervisorctl status | Web UI run list / runwisp tui |
supervisorctl reread + update | runwisp validate + daemon restart |
Gotchas
Services aren't cron-driven
A [services.*] block with a cron field is rejected at load. supervisord's "program that sleeps until the next minute" maps to [tasks.*] with cron = "..." and on_overlap = "skip".
Names share one namespace
[tasks.api] and [services.api] can't coexist. Migrating a fleet of [program:*] blocks where the same name is reused means a one-time disambiguation pass.
Replicas share environment
Replicas are indistinguishable from inside the process: no per-replica env var, no %(process_num)s-style substitution. If replicas need to differ (different config files, different ports), declare N separate services with the differences baked into run.
Reload is daemon-wide
No supervisorctl update api-worker. Edit, runwisp validate, restart. A parse error fails the boot before the daemon opens its port.
FAQ
Services are implicitly restart = "always". A replica that exits is restarted after restart_delay with the configured backoff curve. A replica that stays up for backoff_reset_after resets its counter.
Replicas share the service's environment and aren't distinguishable from inside the process — no per-replica index is exposed today. If replicas need to differ (different config files, different ports), declare them as N separate services.
Unified per service. Stdout and stderr from every replica land in the same per-service log, with each replica's lines tagged in the Web UI. If grep-by-replica on disk matters, declare N services with distinct names instead of instances = N.
Less surgical. RunWisp restarts the whole daemon to pick up a TOML edit. Validate first; boot fails fast on a parse error before the daemon opens its port.
Yes. They don't share state, sockets, or log paths unless you point them at the same files. Migrate one [program:*] at a time; roll back by reversing the same two steps.
More comparisons: RunWisp vs cron · RunWisp vs systemd timers · RunWisp vs PM2 .
One binary supervising your processes. No Python in sight.
Copy a program block into a services stanza, set instances, restart the daemon. Replicas come up indexed and visible.