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.
- You want a configurable restart curve, not fixed +1s steps.
- You also have scheduled jobs that don't fit a [program:*] mould.
- Stop should signal the whole process group by default, 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 — and both columns win on something.
| Capability | supervisord | RunWisp |
|---|---|---|
| Runtime-free single binary | ✗needs Python | ✓ |
| Always-on services | ✓ | ✓ |
| Scheduled jobs (cron-shaped) | ✗ | ✓[tasks.*] |
| Replicas in one config | ✓numprocs | ✓instances = N |
| Per-replica index in the command | ✓%(process_num)s | ✓$RUNWISP_INSTANCE_INDEX |
| Configurable restart-backoff curve | ~+1s per retry, startup only | ✓constant / linear / exp |
| FATAL after repeated fast-fails | ✓startretries | ✓restart_attempts |
| Uptime threshold for a good start | ✓startsecs | ✓healthy_after |
| Health-check command | ~superlance add-on | ✓health_check |
| Per-run history rows | ✗ | ✓ |
| Web UI | ~status page, no history | ✓ |
| TUI | ✗supervisorctl is a CLI | ✓ |
| Stop signals the whole process group | ✗opt-in: stopasgroup | ✓default |
| Coalesced failure notifications | ✗ | ✓ |
| Surgical per-program reload | ✓reread + update | ✗reload is daemon-wide |
| Restart policy by exit code | ✓exitcodes | ✓failures = ["23"] + on_failure |
| Event-listener fan-out | ✓ | ✗ |
| Documented RPC API | ✓XML-RPC | ~REST + OpenAPI |
| Per-program user identity | ✓ | ✓user = |
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.
runwisp import supervisordconverts an existing config into an annotated TOML with# TODOs.- Every replica start and exit is a row in the Web UI with exit code and captured streams.
- Restart backoff you pick (constant, linear, exponential), plus
restart_attemptsbefore FATAL and an optionalhealth_checkcommand. - Tasks (
cron,on_overlap, retries) split from services at the schema level. depends_onfor health-gated boot order and reverse-order shutdown.- Stop signals the whole process group by default.
- Slack, Telegram, Discord, ntfy, email and webhook notifiers coalesce flapping into one alert plus a summary.
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"
user = "api" The version that runs in prod
Three replicas of a queue worker. numprocs and process_name templating 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"
run = "/usr/local/bin/worker"
instances = 3 # each gets RUNWISP_INSTANCE_INDEX
user = "api"
restart_delay = "2s"
restart_backoff = "exponential" # supervisord: fixed +1s steps
healthy_after = "5s" # uptime before a start counts as stable
restart_attempts = 5 # fast-fail this many times, then go FATAL
stop_signal = "SIGTERM"
graceful_stop = "20s" # finish the in-flight job; keep [daemon] shutdown_timeout above this
keep_runs = 500
notify = ["slack-ops"] Migration cheat sheet
supervisord field on the left, runwisp.toml field on the right. `runwisp import supervisord` does most of it for you.
| supervisord | RunWisp |
|---|---|
[program:api] | [services.api] (always-on) or [tasks.api] |
command = | run = |
numprocs = 3 | instances = 3 |
%(process_num)s | $RUNWISP_INSTANCE_INDEX in the command (counts from 0) |
autorestart = true | Implicit; services restart after any exit |
autorestart = unexpected + exitcodes | restart = "on_failure"; list failing exits in failures by hand |
startsecs = 5 | healthy_after = "5s" |
startretries = 5 | restart_attempts = 5 (then FATAL) |
priority = 999 | priority (boot order) or depends_on |
stopsignal = TERM | stop_signal = "SIGTERM" |
stopwaitsecs = 20 | graceful_stop = "20s" |
stopasgroup / killasgroup | Default (process-group-wide) |
stdout_logfile = … | Captured per run; download from Web UI |
user = api | user = "api" (daemon running as root) |
supervisorctl status | runwisp status, Web UI, or runwisp tui |
supervisorctl reread + update | runwisp reload (daemon-wide, validate-first) |
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 differ by one env var
Each instance gets RUNWISP_INSTANCE_INDEX (0, 1, 2, …), so %(process_num)s becomes $RUNWISP_INSTANCE_INDEX in the command. Everything else is shared. If replicas need different config files or ports, derive them from the index or declare N separate services.
Reload is daemon-wide, not per-program
There's no supervisorctl update api-worker. runwisp reload (or SIGHUP) re-reads the whole config without a restart and validates first (a bad edit is rejected and the running set is untouched), but it acts on the daemon as a unit, not one program at a time.
FAQ
What are the restart-on-failure semantics; does instances behave like autorestart?
Can each replica get its own environment, like supervisord's process_num?
Are logs split per replica?
How do reload semantics compare to supervisorctl reread + update?
Can I run RunWisp and supervisord side-by-side during migration?
Does RunWisp support supervisord's event listeners?
One binary supervising your processes. No Python in sight.
Run runwisp import supervisord on your config, check the TODOs, and watch each replica come up as its own run in the Web UI.
curl -fsSL https://get.runwisp.com | sh