Comparison · vs cron
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.
- Your DST line silently fires twice or zero times.
- Failures should ping Slack, not the local MTA.
- You want retries with backoff per task, not per script.
- 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 reloads on save — 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, both columns win on something.
| Feature | cron | RunWisp |
|---|---|---|
| Run history per firing | | SQLite rows |
| Captures stdout / stderr | | |
| Retries with backoff | | constant / linear / exp |
| Failure notifications (Slack, etc.) | | |
| Coalesced alerts (no notify storms) | | |
| Catch-up after downtime | | latest / all / skip |
| DST fall-back safety | fires twice | deduped |
| DST spring-forward safety | skipped silently | fires at next valid minute |
| Overlap policy | you write flock | |
| Crashed-run detection on boot | | |
| Alerts on a run missed during downtime | silent | notify_on_missed |
| Run once at boot | @reboot | run_on_start |
| Web UI / dashboard | | |
| TUI over SSH | | |
| Live config reload | | runwisp reload / SIGHUP |
| Pre-installed everywhere | | 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.
-
crontab -ereloads on save; RunWisp needs an explicitrunwisp reload. - One
RUN echo … | crontab -line fits in any Dockerfile. - 50 years of weird-edge-case forum posts to crib from.
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.
- DST is named behaviour, not an annual silent bug.
-
catch_up = "latest"/all/skipinstead of "hope nobody rebooted." - A firing missed while the daemon was down lands as a
missedrun and a failure alert — cron just stays quiet. - Slack / Telegram / Discord / email / webhook notifiers coalesce flapping into one alert plus a summary.
-
runwisp reload(or SIGHUP) picks up add / change / remove edits without a restart — validate-first, so a bad edit leaves the live set untouched. - Native
user = "deploy"per task when the daemon runs as root;run_on_startcovers@reboot. - 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" Production-grade
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
MAILTO=ops@example.com
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 on $(hostname)" ops@example.com < /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_on_failure = ["slack-ops"]
run = "/usr/local/bin/backup.sh" Migration cheat sheet
Concrete mappings from cron concepts to runwisp.toml fields.
| cron | RunWisp |
|---|---|
MAILTO=ops@example.com | notify_on_failure = ["slack-ops"] |
flock -n /var/run/x.lock | on_overlap = "skip" |
|| mailx … on $(hostname) | notify_on_failure + 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 = true on the task |
0 3 * * * on a host that reboots | catch_up = "latest" (default) |
*/30 * * * * * (seconds) | cron = "*/30 * * * * *" (six-field) or @every 30s |
crontab -e (live reload) | runwisp reload (validates first, no restart) |
30 2 * * * across DST | Deduped on fall-back; fires at next valid minute on spring-forward |
User= in the system 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.
catch_up defaults to latest; set skip for probes
A health probe wants fresh state, not a stale catch-up the moment the daemon comes back. Set catch_up = "skip" for probes and metrics scrapes. For idempotent jobs (rsync, snapshot rotation, mail digests) the default latest 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
Yes. RunWisp doesn't touch /etc/crontab, /var/spool/cron, or anyone's user crontab. Move one task at a time: comment the cron line, restart the daemon, watch it land in the Web UI. If two copies of the same job firing in the same minute would be a problem during the cut-over, set on_overlap = "skip" on the RunWisp side and leave the cron line dormant until you're ready to delete it.
Yes. runwisp import cron reads a crontab — a file path, or piped in with crontab -l | runwisp import cron — and emits an annotated runwisp.toml, with inline # TODO comments for anything that needs a human decision (overlap policy, retries, identity). Review it, then run it.
Run it as whoever owns the work. When the daemon runs as root, set user = "deploy" (or "deploy:deploy", name or numeric id) per task or service to drop privileges. Otherwise every task runs as the daemon's user.
Cron expressions evaluate in [scheduler] timezone or per-task timezone =. Fall-back fires once (the suppressed firing is still in run history); spring-forward fires once at the next valid minute. UTC is unaffected. With catch_up = "skip" a fall-back firing simply doesn't run twice; with catch_up = "latest" (default) the duplicate is suppressed and recorded in history as such, so the behaviour is auditable rather than silent.
Yes. One Go binary, no runtime. Same TOML, same SQLite path conventions, same Web UI port on Linux, macOS, WSL, and Docker. The only platform-specific thing is the autostart wrapper (systemd unit on Linux, launchd plist on macOS, init script in containers), and the operations/autostart docs page has the unit file for each. cron's macOS / WSL story is patchy; this one isn't.
More comparisons: RunWisp vs supervisord · RunWisp vs systemd timers · RunWisp vs PM2 .
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.