Comparison · vs systemd timers

systemd timers schedule well. RunWisp makes every run easier to operate.

Keep systemd when its cgroups, sandboxing, and place at PID 1 matter. Try RunWisp when the harder problem is seeing what ran, why it failed, what it printed, and who was alerted.

Verdict

Should you switch?

The 10-second version. Skim, decide, scroll down for the receipts.

Try RunWisp if

  • You want scheduled tasks and resident services in one operational view.
  • You want each firing recorded with status, duration, and captured output.
  • Retries, overlap, catch-up, and alert routing should be explicit task settings.
  • A Web UI, TUI, and API would make routine inspection and triggering faster.
  • The same configuration needs to work across Linux, macOS, WSL, or containers.

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 (weekday ranges, last-day-of-month) is in active use.
Head to head

Different strengths, at a glance

Yes / no — and both columns win on something.

Capability systemd timers RunWisp
Cross-platform (macOS / WSL / Alpine) ✗ ✓
One file per scheduled task ✗.timer + .service ✓
Per-firing run rows ~journal invocations (v257+) ✓normalized run history
Captured stdout/stderr per run ✓journal; per-run filter v257+ ✓downloadable
Retries with backoff curve ~RestartSteps= (v254+), no attempt cap ✓
Slack / Telegram / email notifiers ~wire through OnFailure= ✓built in
Coalesced flap notifications ✗ ✓
Overlap policy ~active unit is not restarted ✓skip / queue / kill
Catch-up after downtime ✓Persistent=true ✓catch_up = 0 / 1 / N
Web UI ~Cockpit, no run history ✓
TUI over SSH ✗ ✓
cgroup resource limits ✓ ✗
Per-task identity (User=) ✓+ DynamicUser ✓user =
Schedule grammar ✓OnCalendar, ranges and ~ ~6-field cron + @every
Built-in jitter ✓RandomizedDelaySec ✓jitter
Already on the box (no install) ✓ ✗
Daemon needs supervising itself ✗is PID 1 ✓
Trade-offs

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) and last-day-of-month schedules.
  • Nothing extra to install on a systemd distribution.

What RunWisp adds

  • Same daemon, same TOML on Linux, macOS, WSL, Docker.
  • Successful, failed, start-failed, missed, and skipped outcomes share one queryable model across the Web UI, TUI, CLI, and API.
  • catch_up = 1 / N / 0 instead of Persistent= or its absence; a tick missed while down lands as a missed run and an alert.
  • on_overlap = "skip" / "queue" / "kill" 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, ntfy, email or a 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

The simple case

The same nightly backup at 03:00, expressed as a native systemd unit pair or one RunWisp task.

.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
[tasks.backup]
cron = "0 3 * * *"
run  = "/usr/local/bin/backup.sh"
Production-grade

The version that runs in prod

Both versions add production concerns explicitly: downtime, jitter, failures, identity, retries, and retention.

.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
RuntimeMaxSec=55min
Restart=on-failure
RestartMode=direct
RestartSec=30s
RestartSteps=4
RestartMaxDelaySec=5m
StandardOutput=journal
StandardError=journal
runwisp.toml (production)
# runwisp.toml, production
[tasks.backup]
description       = "Snapshot the data volume to off-site storage"
cron              = "0 3 * * *"
catch_up          = 1                 # 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            = ["slack-ops"]
run               = "/usr/local/bin/backup.sh"
Migration

Translate one task, not your whole system

Start with a low-risk timer. RunWisp can import systemd units into annotated TOML, with TODOs where the semantics need a human decision.

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 = 1 (default)
OnFailure=alert.service notify = ["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 --list-invocations Normalized run history in Web UI, TUI, CLI, and API
OnBootSec= run_on_start = "boot" (once per machine boot)
OnUnitActiveSec= cron = "@every …"
CPUQuota= / MemoryMax= Wrap with systemd-run --scope
RuntimeMaxSec=55min timeout = "55m"
RandomizedDelaySec=300 jitter = "5m"
Gotchas

Gotchas

Per-task user, but no DynamicUser

When the daemon runs as root, user = "backup" (or "backup:backup", name or numeric id) runs that task or service as another uid. What you don't get is systemd's DynamicUser=, namespace isolation, or per-unit sandboxing; if those are load-bearing, the unit file 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.

Run RunWisp under systemd

On a systemd host, keep PID 1 doing what it does well. runwisp service install writes a Type=simple unit running runwisp daemon with Restart=on-failure; --print emits it without installing. In a container, use the entrypoint.

FAQ

FAQ

Do I need to migrate a timer to try RunWisp?
No. Run npx runwisp demo (or bunx runwisp demo) to launch a temporary, fully populated instance with run history, logs, live services, and the dashboard. It does not touch your systemd units and deletes its temporary data on exit.
Can the RunWisp daemon itself run under systemd?
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.
Is OnCalendar= syntax supported by RunWisp?
Not directly. RunWisp parses standard cron, five fields, or six with an optional leading seconds field (*/30 * * * * * for every 30s), plus @hourly / @daily / @every aliases. Most OnCalendar= specs translate one-for-one. OnBootSec= maps to run_on_start = "boot"; OnUnitActiveSec= maps to cron = "@every …".
Does systemd have per-invocation history?
Yes. The journal has carried trusted invocation IDs since systemd v233, and v257 added journalctl --list-invocations and --invocation for listing and filtering individual unit runtime cycles. RunWisp's distinction is the normalized operator workflow: scheduler outcomes such as missed and skipped work, task status, duration, output, controls, and alerts share one model across its Web UI, TUI, CLI, and API.
How is failure surfaced compared to OnFailure= unit dispatch?
Per-task notify routes (in-app, Slack, Telegram, email). Outbound notifiers coalesce by (task, event, end reason) inside coalesce_window (default 1h), with a window-close summary.
What's the boot-time catch-up story compared to Persistent=true?
catch_up = 1 is the equivalent default. Set 0 for no catch-up, or N to replay up to N missed ticks (with on_overlap = "queue" when N is above 1).
Are there built-in resource limits like CPUQuota and MemoryMax?
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.
Get started
GPL 3.0 · no accounts · free to self-host

Try the operating workflow before moving a timer.

Start with npx runwisp demo or bunx runwisp demo: a temporary populated instance that leaves your systemd units alone. If the run history and controls earn their keep, import one low-risk timer first.

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