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.
- 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 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 | rides notify_on_failure |
| Run once at boot | @reboot | run_on_start |
| Retire cron in one command | | sudo runwisp takeover |
| Reads your existing crontabs as-is | its own files | [daemon] include_cron |
| crontab -e keeps working | | re-read on reload |
| No double-firing while cron is still live | both fire | jobs held |
| 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. - On Linux + systemd,
sudo runwisp takeoverdoes the whole cutover in one command: imports your crontabs, installs the service, stops and masks cron — and rolls the whole thing back if any step fails. -
[daemon] include_cronreads real crontabs as live tasks, so you get history, captured output, and alerts without rewriting a line. Keep usingcrontab -e. - While a system cron is still alive, cron-sourced jobs are held — loaded and listed, not fired — so nothing runs twice mid-migration.
- 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
[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 on $(hostname)" [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_on_failure = ["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_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 |
| 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; 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.
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 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, and you don't have to keep the two sets in sync by hand. Point [daemon] include_cron at the crontabs you already have and RunWisp loads every job in them — but while a system cron daemon is still running and owns those files, RunWisp holds them: it lists them and shows their schedules without firing them, because cron already is. Nothing runs twice, and you never set or clear a hold — it's re-derived from the box on every load. Held jobs show a ⏸ marker in runwisp status and the TUI, a badge in the Web UI, and held_by: "cron" in the API, and you can still runwisp exec one on demand to watch it under RunWisp first. Stop cron and RunWisp picks the jobs up within a minute; sudo runwisp takeover does it instantly. Hand-written tasks in your own runwisp.toml are never held — for those, comment the cron line out yourself.
Three routes, depending on how much you want to own. sudo runwisp takeover (Linux + systemd) is the whole migration in one command — it writes a config that reads your crontabs live, installs RunWisp as a service, then stops and masks cron, atomically, unmasking cron again if the last step fails. [daemon] include_cron is the same reading half without retiring cron: your crontabs stay the source of truth and crontab -e plus runwisp reload is the workflow. runwisp import cron converts instead of reads — point it at a file or pipe crontab -l into it and it prints an annotated runwisp.toml on stdout with a per-job summary on stderr (mapped cleanly, changed, needs a fix, already yours), --dry-run to preview, -o to write one flat file, or --write to stage the jobs in runwisp.d/imported.toml so your hand-written config is untouched. Either way, runwisp promote <task> later graduates one job into your own runwisp.toml and comments out the crontab line it came from.
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.