launchd Calendar Catch-up

When you launchctl load a LaunchAgent that has a StartCalendarInterval whose scheduled slot has already passed since last run, launchd fires it immediately to “catch up.” RunAtLoad=false does 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 StartCalendarInterval that 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 + bootstrap with 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.