RunWisp

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.

Quick start GitHub

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 / MemoryMax per 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.

.timer + .service
.timer + .service
# /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
runwisp.toml
# 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.

.timer + .service (production)
.timer + .service (production)
# /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)
runwisp.toml (production)
# 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

FAQ

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.