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.
  • DynamicUser= and per-unit sandboxing are load-bearing.
  • Adding any daemon beyond PID 1 is one moving part too many.
  • OnCalendar's richer grammar (ranges, weeks of month) 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-task identity (User=)
+ DynamicUser
user =
Schedule grammar
OnCalendar rich
~ 6-field cron + @every
Built-in jitter
RandomizedDelaySec
jitter
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 DynamicUser=, sandboxing, and namespace isolation.
  • 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; a tick missed while down lands as a missed run and an alert.
  • on_overlap = "skip" / "queue" / "terminate" as a named field.
  • jitter staggers tasks that share a fire time; user = drops to another uid; run_on_start covers OnBootSec.
  • Notifiers route to Slack / Telegram / Discord / email / webhook 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 user = "backup" (daemon running as root)
journalctl -u backup.service Run rows in Web UI; per-run log download
OnBootSec= run_on_start = true
OnUnitActiveSec= cron = "@every …"
CPUQuota= / MemoryMax= Wrap with systemd-run --scope
RandomizedDelaySec=300 jitter = "5m"

Gotchas

FAQ

More comparisons: RunWisp vs cron · RunWisp vs supervisord · RunWisp vs PM2 .

Or see what RunWisp is in one page →

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.