

Stop Using cron on macOS
cron skips jobs when your Mac is asleep. launchd doesn't. Here's how to convert your cron jobs to launchd and never miss a scheduled task again.
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 normallyplaintextThis 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:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.yourname.weekly-cleanup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/you/scripts/cleanup.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>2</integer>
<key>Hour</key>
<integer>10</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/you/scripts/cleanup.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/scripts/cleanup.log</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>xmlWeekday 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.plistbashVerify it registered:
launchctl list | grep yournamebashYou’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>xmlFor interval-based jobs, use StartInterval instead:
<!-- Every 5 minutes -->
<key>StartInterval</key>
<integer>300</integer>xmlInterval jobs measure time since last run, so they always catch up after a machine wakes.
Common gotchas#
| Issue | Cause | Fix |
|---|---|---|
| Job doesn’t fire | plist not loaded | launchctl load <plist> |
| ”already loaded” error | reloading without unloading first | launchctl unload, then load |
| Script fails silently | wrong path in ProgramArguments | use absolute paths everywhere |
| No log output | StandardOutPath not set | add StandardOutPath and StandardErrorPath |
| Job fires on load unexpectedly | RunAtLoad: true | set to false |
| Job lost after reboot | plist not in LaunchAgents | confirm 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.plistbashAfter editing a plist, unload then reload:
launchctl unload ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plist
launchctl load ~/Library/LaunchAgents/com.yourname.weekly-cleanup.plistbashIs 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.