srmdn.

Back

Stop Using cron on macOSBlur image

If you’re scheduling tasks on macOS with cron, you’re probably losing jobs without knowing it. Unless your Mac never sleeps.

cron was built for always-on servers. If your machine is asleep or off when the scheduled time hits, cron skips the job and moves on. No retry, no catch-up, no log entry. The task just doesn’t run. On a laptop or a desktop you shut down at night, that’s a silent failure mode.

launchd is macOS’s native scheduler, and it handles this differently: if a job was due while the machine was off or asleep, it fires on the next wake. That’s the only behavior difference that matters for most people.

How they handle a missed job#

cron:
  Monday 10am — Mac asleep — job skipped — nothing happens
  Tuesday — job runs normally (Monday is gone forever)

launchd:
  Monday 10am — Mac asleep — job queued
  Monday 12pm — Mac wakes — job fires immediately
  Tuesday — job runs normally
plaintext

This only matters if your machine actually misses the scheduled time. If it doesn’t, both schedulers behave identically.

Writing a launchd job#

launchd jobs are defined in .plist XML files. Per-user jobs live in ~/Library/LaunchAgents/. System-wide ones go in /Library/LaunchDaemons/.

Here’s a job that runs a shell script every Tuesday at 10am:

Weekday counts from Sunday=0 to Saturday=6. RunAtLoad: false means it won’t fire immediately when you load it, only at the scheduled time.

Save it to ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plist, then load it:

launchctl load ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plist
bash

Verify it registered:

launchctl list | grep yourname
bash

You’ll see a - in the PID column and 0 as the exit code. That’s correct for a scheduled job that’s currently idle.

Scheduling options#

StartCalendarInterval covers time-based schedules. Omitting a key treats it as a wildcard:

<!-- Every day at 9am -->
<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key><integer>9</integer>
    <key>Minute</key><integer>0</integer>
</dict>

<!-- Every hour at :30 -->
<key>StartCalendarInterval</key>
<dict>
    <key>Minute</key><integer>30</integer>
</dict>
xml

For interval-based jobs, use StartInterval instead:

<!-- Every 5 minutes -->
<key>StartInterval</key>
<integer>300</integer>
xml

Interval jobs measure time since last run, so they always catch up after a machine wakes.

Common gotchas#

IssueCauseFix
Job doesn’t fireplist not loadedlaunchctl load <plist>
”already loaded” errorreloading without unloading firstlaunchctl unload, then load
Script fails silentlywrong path in ProgramArgumentsuse absolute paths everywhere
No log outputStandardOutPath not setadd StandardOutPath and StandardErrorPath
Job fires on load unexpectedlyRunAtLoad: trueset to false
Job lost after rebootplist not in LaunchAgentsconfirm the file is in ~/Library/LaunchAgents/

Always use absolute paths in ProgramArguments. launchd doesn’t inherit your shell’s PATH, so /bin/bash instead of just bash.

Editing and unloading#

To stop a job and remove it from the scheduler:

launchctl unload ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plist
bash

After editing a plist, unload then reload:

launchctl unload ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plist
launchctl load ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plist
bash

Is This Right for You?#

The honest answer depends on what kind of machine you’re running.

Laptop or personal desktop: Switch to launchd. These machines sleep, get shut down, travel. cron will silently drop jobs whenever the lid is closed at the wrong time. launchd catches up on the next wake.

Mac mini homelab or always-on server: cron is fine. A Mac mini running 24/7 with sleep disabled is functionally identical to a Linux server. The machine is always there when the scheduled time hits, so cron’s lack of catch-up logic never matters. There’s no practical reason to migrate.

Mac mini that sometimes sleeps or loses power: Use launchd. The moment uptime becomes unreliable, cron becomes unreliable with it.

The rule is simpler than it sounds: if your machine has predictable uptime, cron works. If it doesn’t, launchd is the safer default. Most Macs that people actually sit in front of fall into the second category. Most Mac minis running home servers fall into the first.

If you’re already running cron jobs that work consistently, don’t migrate them. The conversion is straightforward, but it’s only worth doing if you have an actual reason.

Enjoyed this post?

Get Linux tips, sysadmin war stories, and new posts delivered to your inbox.

No spam. Unsubscribe anytime.

Stop Using cron on macOS
https://srmdn.com/blog/launchd-vs-cron-macos
Author srmdn
Published at April 6, 2026