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.

Verdict

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

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

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 -e is picked up on its own within a minute; RunWisp needs an explicit runwisp 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_up replays what a reboot ate. A firing missed while the daemon was down lands as a missed run 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_cron reads your real crontabs as live tasks, so history, captured output and alerts need no rewrite. crontab -e keeps working.
  • sudo runwisp takeover does 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

The simple case

Nightly backup at 03:00. One line, one stanza.

crontab
# /etc/cron.d/nightly-backup
0 3 * * *  root  /usr/local/bin/backup.sh
runwisp.toml
# runwisp.toml
[tasks.nightly-backup]
cron = "0 3 * * *"
run  = "/usr/local/bin/backup.sh"
Production-grade

The version that runs in prod

Same backup, hardened. flock + redirect + || mailx on the left; named fields on the right.

crontab (production)
# /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 (production)
# 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

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

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

FAQ

Can RunWisp run alongside my existing crontab while I migrate?
Yes. Point [daemon] include_cron at the crontabs you already have and RunWisp loads every job in them. While a system cron daemon is still running, those jobs are held: listed with their schedules (a ⏸ marker in runwisp status and the TUI, a badge in the Web UI) but not fired, because cron already is. Nothing runs twice, and you never set the hold yourself. You can still runwisp run a held job to watch it under RunWisp first. Stop cron and RunWisp picks the jobs up within a minute; sudo runwisp takeover does it instantly. Tasks in your own runwisp.toml are never held, so comment those cron lines out yourself.
Is there an import command for an existing crontab?
Three routes. sudo runwisp takeover (Linux + systemd) does the whole migration in one command: it writes a config that reads your crontabs live, installs the service, then stops and masks cron, and unmasks it again if the last step fails. [daemon] include_cron is the reading half without retiring cron, so crontab -e stays your workflow. runwisp import cron converts instead of reads: pipe crontab -l into it for an annotated runwisp.toml with a per-job summary, or add --write to stage the jobs in runwisp.d/imported.toml and leave your hand-written config alone. runwisp promote later moves one job into your own runwisp.toml and comments out the crontab line.
Does RunWisp need to run as root?
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.
How does RunWisp handle timezones and DST?
Cron expressions evaluate in the daemon timezone or a per-task timezone. On fall-back the task runs once and the second 02:30 is recorded in history as dst_skipped. On spring-forward it runs once, shifted by the gap, so 02:30 runs at 03:30. UTC is unaffected, and @every schedules count real elapsed time.
Does the same binary work on macOS and WSL?
Yes. One Go binary, no runtime, the same TOML and Web UI port on Linux, macOS, WSL and Docker. The autostart wrapper is the only platform-specific part: a systemd unit on Linux, a launchd plist on macOS, the container entrypoint in Docker. Native Windows isn't supported; use WSL.
Get started
GPL 3.0 · no accounts · free to self-host

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.

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