RunWisp vs cron: every fire, captured.
A crontab line and the equivalent RunWisp TOML, with the operational behaviour where they diverge: catch-up policy, DST handling, crashed-run detection.
Should you switch?
The 10-second version. Skim, decide, scroll down for the receipts.
Switch to RunWisp if
- You've ever asked: did the 03:00 backup actually run?
- Reboots eat firings and you find out hours later.
- You run Alpine, where crond doubles or drops a 02:30 job on DST nights.
- Failures should ping Slack, not the local MTA.
- You want retries with backoff per task, not per script.
- You'd rather keep the crontabs you have than rewrite them.
- Same host runs macOS, WSL, or a no-systemd container.
Stay on cron if
- Three lines in /etc/crontab and none ever fail.
- A crontab edit is picked up within a minute, no command to run.
- Container is one RUN echo … | crontab - line.
- Adding a daemon costs more than the problem.
Yes / no, at a glance
Yes / no — and both columns win on something.
| Capability | cron | RunWisp |
|---|---|---|
| Run history per firing | ✗ | ✓SQLite rows |
| Captures stdout / stderr | ~mailed, not stored | ✓ |
| Retries with backoff | ✗ | ✓constant / linear / exp |
| Failure notifications (Slack, etc.) | ~mails output, not exit codes | ✓ |
| Coalesced alerts (no notify storms) | ✗ | ✓ |
| Catch-up after downtime | ~anacron: daily and slower only | ✓catch_up = 0 / 1 / N |
| DST fall-back safety | ~Debian: once, BusyBox: twice | ✓deduped |
| DST spring-forward safety | ~Debian: runs late, BusyBox: skipped | ✓shifted by the gap |
| Overlap policy | ✗you write flock | ✓ |
| Crashed-run detection on boot | ✗ | ✓ |
| Alerts on a run missed during downtime | ✗silent | ✓rides notify |
| Run once at boot | ✓@reboot | ✓run_on_start = "boot" |
| Reads your existing crontabs as-is | ✓its own files | ✓[daemon] include_cron |
| Web UI / dashboard | ✗ | ✓ |
| TUI over SSH | ✗ | ✓ |
| Live config reload | ✓automatic, within a minute | ✓runwisp reload / SIGHUP |
| Pre-installed everywhere | ~most servers, not slim images | ✗ship a binary |
| Sub-minute (seconds field) | ✗BSD/cronie no | ✓6-field cron |
| macOS / WSL / Docker reach | ~varies | ✓ |
| Per-task user identity | ✓ | ✓user = (daemon as root) |
Pros & cons
What cron still does better
- Already installed; nothing extra to ship in the base image.
- Debian cron and cronie already handle DST for fixed-time jobs: once on fall-back, right after the jump on spring-forward.
crontab -eis picked up on its own within a minute; RunWisp needs an explicitrunwisp reload.- One
RUN echo … | crontab -line fits in any Dockerfile. - Decades of weird-edge-case forum posts to crib from.
- Mails any output to you with a working MTA and zero config.
What RunWisp adds
- Every firing is a row with status, duration, exit code, and captured logs.
- Retries and backoff live on the task, not in a wrapper script.
- The same DST behaviour on every box, Alpine included, with the suppressed repeat recorded in history.
catch_upreplays what a reboot ate. A firing missed while the daemon was down lands as amissedrun and a failure alert; cron stays quiet.- Slack, Telegram, Discord, ntfy, email and webhook notifiers coalesce flapping into one alert plus a summary.
[daemon] include_cronreads your real crontabs as live tasks, so history, captured output and alerts need no rewrite.crontab -ekeeps working.sudo runwisp takeoverdoes the cutover in one command (Linux + systemd) and rolls back if any step fails. While cron is still alive its jobs are held, so nothing runs twice.- One binary on Linux, macOS, WSL, Docker; no Python, no MTA.
The simple case
Nightly backup at 03:00. One line, one stanza.
# /etc/cron.d/nightly-backup
0 3 * * * root /usr/local/bin/backup.sh # runwisp.toml
[tasks.nightly-backup]
cron = "0 3 * * *"
run = "/usr/local/bin/backup.sh" The version that runs in prod
Same backup, hardened. flock + redirect + || mailx on the left; named fields on the right.
# /etc/cron.d/nightly-backup, the version that actually runs in prod
# cron has no line continuation, so this is one very long line
[email protected]
0 3 * * * root flock -n /var/run/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 || mailx -s "[FAIL] nightly-backup" [email protected] < /var/log/backup.log # runwisp.toml, the version that actually runs in prod
[tasks.nightly-backup]
description = "Snapshot the data volume to off-site storage"
cron = "0 3 * * *"
on_overlap = "skip" # never two backups at once
timeout = "55m" # die before the next firing
retry_attempts = 2
retry_delay = "30s"
retry_backoff = "exponential"
keep_runs = 60 # two months of history
notify = ["slack-ops"]
run = "/usr/local/bin/backup.sh" Migration cheat sheet
Concrete mappings from cron concepts to runwisp.toml fields.
| cron | RunWisp |
|---|---|
[email protected] | notify = ["slack-ops"] |
flock -n /var/run/x.lock | on_overlap = "skip" |
|| mailx … on $(hostname) | notify + coalesce_window |
wrapper loop with sleep + counter | retry_attempts + retry_backoff |
>> /var/log/job.log 2>&1 | Captured automatically; download per-run from Web UI. |
@reboot | run_on_start = "boot" on the task (true runs on every daemon start) |
0 3 * * * on a host that reboots | catch_up = 1 (default) |
* * * * * sleep 30; cmd (sub-minute hack) | cron = "*/30 * * * * *" (six-field) or @every 30s |
| Every crontab on a Linux + systemd box | sudo runwisp takeover — imports them, installs the service, masks cron, rolls back on failure |
/etc/crontab, /etc/cron.d/*, user spools | [daemon] include_cron = ["/etc/crontab", "/etc/cron.d/*"] — read live, never rewritten |
crontab -e (live reload) | Still crontab -e; runwisp reload re-reads the file (validates first, no restart) |
| A crontab, plus a runwisp.toml you already hand-write | runwisp import cron --write — stages into runwisp.d/imported.toml, leaves yours alone |
| One cron job you want to own as TOML in git | runwisp promote <task> — copies the block out and comments the source line |
30 2 * * * across DST | Deduped on fall-back; shifted forward by the gap on spring-forward (02:30 runs at 03:30) |
User column in /etc/crontab | user = "deploy" (daemon running as root) |
Gotchas
Six-field cron buys you seconds
A leading seconds field is optional: */30 * * * * * fires every 30 seconds aligned to the wall clock, and @every 30s still works. Existing five-field specs and aliases (@hourly, @daily) are unchanged. Quartz operators (L, W, #) are not parsed.
Reload is explicit and validate-first
Edit runwisp.toml, then runwisp reload (or send SIGHUP) to pick up task add / change / remove without a restart. A bad edit or a restart-only change is rejected and the running set is left untouched. Newly added tasks don't fire run_on_start or catch up on missed ticks at reload.
takeover is Linux + systemd, as root
Retiring cron means masking it, which needs systemctl and sudo. On macOS or a box without systemd, runwisp takeover can't do the cron half — convert with runwisp import cron and comment the old lines out yourself. Masking is reversible: runwisp service uninstall unmasks cron and starts it again.
catch_up defaults to 1; set 0 for probes
A health probe wants fresh state, not a stale catch-up the moment the daemon comes back. Set catch_up = 0 for probes and metrics scrapes. For idempotent jobs (rsync, snapshot rotation, mail digests) the default 1 is what you want: exactly one make-up firing after downtime, not silence.
Tasks run as the daemon's user unless you say otherwise
cron lines often run as root because the system crontab does. RunWisp tasks run as the daemon's user by default; when the daemon runs as root, set user = "deploy" (or "deploy:deploy", name or numeric id) per task to drop to another uid.
FAQ
Can RunWisp run alongside my existing crontab while I migrate?
Is there an import command for an existing crontab?
Does RunWisp need to run as root?
How does RunWisp handle timezones and DST?
Does the same binary work on macOS and WSL?
Move one crontab line. See it run.
Install the binary, paste a five-field cron expression into runwisp.toml, watch the first firing land in the Web UI.
curl -fsSL https://get.runwisp.com | sh