cron vs launchd on macOS

The short answer: launchd for anything you care about, cron for a two-minute job you will read back yourself.

Side by side

 cronlaunchd
Where it livesOne line per job in your crontabOne property list per job in ~/Library/LaunchAgents and the system directories
Writing a schedule30 4 * * 1-5, with lists, ranges, steps and @dailyA dictionary of integers; no ranges or steps, so several times means several dictionaries
Asleep at the timeThe run is lostThe job runs when the Mac wakes
PATH/usr/bin:/bin unless the crontab sets one/usr/bin:/bin:/usr/sbin:/sbin unless the job sets one
OutputMailed to /var/mail/<user>Discarded unless you set StandardOutPath and StandardErrorPath
HistoryNone. Nothing records a cron runlaunchctl print gives a run count and the last exit code
Running nowRun the command yourselflaunchctl kickstart
Apple's positionPresent, works, documented as legacyThe supported way to schedule work

The three differences that decide it

Missed runs

A laptop is asleep at 03:00 most nights. cron simply misses that run and waits for tomorrow, which also misses. launchd remembers a calendar job was due and runs it once when the Mac wakes. For a nightly backup on a laptop, that alone settles the argument.

Evidence

When a launchd job misbehaves you can ask it what happened: launchctl print gui/$UID/com.example.job reports its state, its runs count and the last exit code, as your own user with no password. cron keeps nothing, and writes no entries to the unified log at any level, so a cron job that fails silently leaves no trace anywhere except the mail it sent.

Effort

cron wins here and it is not close. A crontab line is twenty characters; the equivalent property list is twenty lines of XML, a Label you must keep unique, and a bootstrap command to remember. That is why most Macs still have both.

Things that surprise people

See what you already have

Most Macs are running both, usually without anyone deciding so. Three commands to find out:

crontab -l                          # your cron jobs
ls ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons
launchctl list | head -40           # what is loaded in your session right now

The agents you did not write are Homebrew's brew autoupdate, Docker, JetBrains, Adobe, backup tools and updaters. launchctl list shows everything loaded, scheduled or not; to see only the scheduled ones you have to open each property list and look for StartCalendarInterval or StartInterval.

Which to use

Use launchd when the job matters: backups, syncs, anything you would want to prove ran. Use cron when you want a line in a file you can read a year from now, the job is harmless if it is skipped, and you will redirect its output to a log yourself. Do not migrate working cron jobs for the sake of it; do write new important ones as agents.

Or stop choosing sides and see both

CronMon lists your crontab and the scheduled launchd agents and daemons together, sorted by when they next run, each schedule written out in English. It is the quickest way to answer "what actually runs at 4am on this Mac", which on most Macs is a question spread across both schedulers.

The CronMon popover showing cron jobs and launch agents in one list, with the next runs above them on a timeline.

Download CronMon