Comparison · vs systemd timers
RunWisp vs systemd timers: one TOML, three platforms, run history included.
A real .timer + .service pair next to the equivalent runwisp.toml. Platform reach, run history, OnCalendar vs cron, what systemd still does that RunWisp doesn't.
Should you switch?
The 10-second version. Skim, decide, scroll down for the receipts.
Switch to RunWisp if
- Your fleet includes macOS, WSL, Alpine, or no-systemd containers.
- You want runs as rows in a table, not "filter journalctl by unit and time."
- One TOML stanza beats writing two unit files and an enable command.
- Failure should fan out to Slack, not dispatch another .service.
- Catch-up after downtime should be a named policy, not Persistent= or its absence.
Stay on systemd timers if
-
CPUQuota/MemoryMaxper task are doing real work. - Per-unit
User=across many distinct identities matters. -
RandomizedDelaySec=jitter is load-bearing. - Adding any daemon beyond PID 1 is one moving part too many.
-
OnCalendar's richer grammar is in active use.
Yes / no, at a glance
Yes / no, both columns win on something.
| Feature | systemd timers | RunWisp |
|---|---|---|
| Cross-platform (macOS / WSL / Alpine) | | |
| One file per scheduled task | .timer + .service | |
| Per-firing run rows | | journalctl filter |
| Captured stdout/stderr per run | ~ in journal | downloadable |
| Retries with backoff curve | ~ Restart=, RestartSec= | |
| Slack / Telegram / email notifiers | OnFailure=unit | |
| Coalesced flap notifications | | |
| Overlap policy | | skip / queue / terminate |
| Catch-up after downtime | Persistent=true | named policy |
| Web UI | | |
| TUI over SSH | | |
| cgroup resource limits | | |
| Per-unit identity (User=) | | ~ sudo wrapper |
| Schedule grammar | OnCalendar rich | ~ 5-field cron + @every |
| Built-in jitter (RandomizedDelaySec) | | |
| Already on the box (no install) | | |
| Daemon needs supervising itself | is PID 1 | |
Pros & cons
What systemd timers still do better
- Already PID 1; no supervisor-of-the-supervisor question.
- Full cgroup surface (
CPUQuota,MemoryMax,IOWeight) per unit. - Per-unit
User=,Group=,DynamicUser=. - Built-in
RandomizedDelaySec=jitter for thundering-herd. -
OnCalendar=ranges (Mon..Fri 09:30, weeks of month). - Nothing extra to install on a systemd distribution.
What RunWisp adds
- Same daemon, same TOML on Linux, macOS, WSL, Docker.
- Every firing is a queryable run row with status, duration, captured streams.
-
catch_up = "latest" / "all" / "skip"instead of Persistent= or its absence. -
on_overlap = "skip" / "queue" / "terminate"as a named field. - Notifiers route to Slack / Telegram / email and coalesce flap storms.
- Schedule travels with
runwisp.toml; back up the file and you back up the schedule. - TUI works over SSH/tmux for headless inspection.
The simple case
Nightly backup at 03:00. Two unit files vs one stanza.
# /etc/systemd/system/backup.timer
[Unit]
Description=Nightly backup timer
[Timer]
OnCalendar=03:00
Unit=backup.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh # runwisp.toml
[tasks.backup]
cron = "0 3 * * *"
run = "/usr/local/bin/backup.sh" Production-grade
Same backup, hardened. Persistent + RandomizedDelaySec + OnFailure on the left; named fields on the right.
# /etc/systemd/system/backup.timer
[Unit]
Description=Nightly backup timer
[Timer]
OnCalendar=03:00
Persistent=true
RandomizedDelaySec=300
Unit=backup.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
OnFailure=backup-alert.service
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh
Restart=on-failure
RestartSec=30s
StandardOutput=journal
StandardError=journal # runwisp.toml, production
[tasks.backup]
description = "Snapshot the data volume to off-site storage"
cron = "0 3 * * *"
catch_up = "latest" # one catch-up run after host downtime
on_overlap = "skip"
timeout = "55m"
retry_attempts = 2
retry_delay = "30s"
retry_backoff = "exponential"
keep_runs = 60
notify_on_failure = ["slack-ops"]
run = "/usr/local/bin/backup.sh" Migration cheat sheet
systemd directive on the left, runwisp.toml field on the right.
| systemd timers | RunWisp |
|---|---|
.timer + .service pair | One [tasks.*] stanza |
OnCalendar=03:00 | cron = "0 3 * * *" |
OnCalendar=Mon..Fri 09:30 | cron = "30 9 * * 1-5" |
Persistent=true | catch_up = "latest" (default) |
OnFailure=alert.service | notify_on_failure = ["slack-ops"] |
Restart=on-failure + RestartSec= | retry_attempts + retry_delay + retry_backoff |
ExecStart= | run = |
User=backup | Wrap run with sudo -u |
journalctl -u backup.service | Run rows in Web UI; per-run log download |
OnBootSec= / OnUnitActiveSec= | Out of scope; not modelled |
CPUQuota= / MemoryMax= | Wrap with systemd-run --scope |
RandomizedDelaySec=300 | Stagger cron expressions manually |
Gotchas
Tasks run as the daemon's user; no per-task User=
Every task and service inherits the daemon's identity. For a single task that needs a different uid, wrap run with sudo -u or runuser. If many tasks each need different identities, systemd's per-unit User= is doing real work for you.
No built-in resource limits
No analogue to CPUQuota or MemoryMax in runwisp.toml. Combine with systemd-run --scope --user --slice=… inside run, or any cgroup-aware launcher.
The daemon needs supervising
systemd is PID 1; RunWisp isn't. The conventional setup on Linux is a Type=simple unit running runwisp daemon with Restart=on-failure. On macOS use launchd; in a container, the entrypoint.
FAQ
Yes, and that's the recommended pattern on a systemd host. A Type=simple unit launching runwisp daemon gives you supervision-of-the-supervisor for free.
Not directly. RunWisp parses 5-field cron plus @hourly / @daily / @every aliases. Most OnCalendar= specs translate one-for-one. Relative timers (OnBootSec=, OnUnitActiveSec=) don't map to cron and are outside scope.
Per-task notify_on_failure routes (in-app, Slack, Telegram, email). Outbound notifiers coalesce by (task, event, end reason) inside coalesce_window (default 1h), with a window-close summary.
catch_up = "latest" is the equivalent default. "all" replays every missed tick up to max_catch_up_runs. "skip" is the no-catch-up explicit choice.
No. Wrap with systemd-run --scope --user --slice=… or another cgroup-aware launcher. On non-systemd hosts the kernel's cgroup primitives are still available; the wiring is yours.
More comparisons: RunWisp vs cron · RunWisp vs supervisord · RunWisp vs PM2 .
Cross the platform line. Keep the run history.
One TOML stanza per scheduled task. Same daemon on Linux, macOS, WSL, and Docker. Wherever you do the work, the schedule travels with it.