launchd Calendar Catch-up
When you
launchctl loada LaunchAgent that has aStartCalendarIntervalwhose scheduled slot has already passed since last run, launchd fires it immediately to “catch up.”RunAtLoad=falsedoes not suppress this. Any agent loaded outside its scheduled window will spawn the program once, right then.
Core claim
StartCalendarInterval is not “fire only at exactly HH:MM” - it is “fire at
HH:MM and also catch up if the slot was missed since the last invocation.”
Loading the plist after the slot has elapsed counts as “missed,” so the
agent fires on load. The only way to load a calendar agent quietly outside
its window is to ensure your invoked program self-detects “wrong time” and
exits cleanly (so KeepAlive=SuccessfulExit=false does not relaunch it).
Evidence
- observed - 2026-05-04 17:15 CT, loaded com.cortanaroi.mk2 (StartCalendarInterval Mon-Fri 08:10), engine spawned immediately and entered “Market not open yet - warming up” mode. Confirmed in mk2-2026-05-04.log:17:15:30.
- derived - Apple
launchd.plist(5)man page: “If the computer is asleep during the time of the next scheduled interval, the job will be started upon wake.” Empirically extends to “if the agent was unloaded during the interval” as well.
When it applies
- Any LaunchAgent or LaunchDaemon with
StartCalendarIntervalthat gets unload/loaded during normal ops (deploy, plist update, debug cycles). - Particularly relevant when reloading multiple plists at end-of-day to prep for the next morning’s scheduled fire.
When it breaks
- If the program itself is market-aware and exits cleanly outside its
trading window AND
KeepAlive=SuccessfulExit=false, the catch-up fire is benign - program runs once, exits, stays quiet until next slot. - If the program is NOT self-aware of time-of-day, catch-up will spawn it at the wrong time and (with KeepAlive on) it will run continuously until the next scheduled slot - which is what you tried to prevent by scheduling in the first place.
How to avoid
- Reload plists during the scheduled window (e.g., reload com.cortanaroi.mk2 at 08:11 CT, not 17:15 CT) - catch-up fire then aligns with intended fire.
- OR: use
launchctl bootout+bootstrapwith care; same semantics apply. - OR: ensure the program self-quiesces outside its window (cortana does this: preflight passes, “Market not open yet - warming up” loop with no orders).
See Also
Timeline
2026-05-04 | observed - Loaded all 3 cortana plists (mk2, mk2-stop,
mk2-watchdog) at 17:15 CT post-market to prep for tomorrow’s 08:10
schedule. com.cortanaroi.mk2 immediately spawned start_mk2.sh despite
RunAtLoad=false. Engine self-detected “Market not open yet” and entered
warming loop - no harm, but a real surprise. Filed this concept so the
next reload-during-off-hours doesn’t re-surprise.