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.

Verdict

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 + update on 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.
Head to head

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 =
Trade-offs

Pros & cons

What supervisord still does better

  • supervisorctl reread + update reloads 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 supervisord converts 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_attempts before FATAL and an optional health_check command.
  • Tasks (cron, on_overlap, retries) split from services at the schema level.
  • depends_on for 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

The simple case

Keep one API process alive. Four INI lines vs one TOML stanza.

supervisord.conf
; /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
# runwisp.toml
[services.api]
run  = "/usr/local/bin/api"
user = "api"
Production-grade

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.

supervisord.conf (production)
; /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 (production)
# 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

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

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

FAQ

What are the restart-on-failure semantics; does instances behave like autorestart?
Services default to restart = "always". An instance that exits is restarted after restart_delay, growing by the backoff curve you pick (constant, linear or exponential). An instance that stays up for healthy_after resets its counter. One that keeps exiting before healthy_after gets restart_attempts failed starts in a row, then goes FATAL: it stops restarting, records a start_failed run and fires an alert (supervisord's startretries → FATAL). For the supervisord default autorestart=unexpected, set restart = "on_failure".
Can each replica get its own environment, like supervisord's process_num?
Replicas share one environment, with one difference: each instance gets RUNWISP_INSTANCE_INDEX (0, 1, 2, …). Use $RUNWISP_INSTANCE_INDEX where you used %(process_num)s. If replicas need different config files or ports, derive them from the index in run, or declare N separate services.
Are logs split per replica?
Each instance is its own run in history, shown as api-worker#1, #2, #3 in the Web UI and TUI, with its own exit code and captured output. Log size and retention settings are shared by the service.
How do reload semantics compare to supervisorctl reread + update?
Less surgical, but no restart. runwisp reload (or SIGHUP) re-reads runwisp.toml and applies service and task add, change and remove in place. It validates first, so a bad edit or a restart-only change is rejected and the live set is untouched. The difference from supervisord is granularity: reload acts on the whole daemon, not one program at a time.
Can I run RunWisp and supervisord side-by-side during migration?
Yes, as long as no program runs under both. Stop a program in supervisord before you start it in RunWisp, or two copies fight over ports and queues. Migrate one [program:*] at a time; roll back by reversing the same steps.
Does RunWisp support supervisord's event listeners?
No. runwisp import supervisord flags [eventlistener] and [fcgi-program] sections so you can replace them another way. Failure alerts, health checks and boot order (depends_on) cover the common listener jobs; for anything else, keep supervisord.
Get started
GPL 3.0 · no accounts · free to self-host

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.

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