cronlag

2026-10-13

Your cron said 06:00. GitHub ran it at 11:50.

Since late August 2026 a lot of on: schedule workflows have been running hours late, or not at all. The community threads (#206019, #206134, #210037) are full of people building the same spreadsheet by hand. cronlag builds it for you.

What the numbers look like

These are real public repos, measured on 2026-10-13 over the previous week:

WorkflowcronResult
robhunter/agentdeals reverify.yml0 6 * * *LATE: median 6h37 after the slot, 0 of 6 runs within an hour
fpgas-online e2e e2e.yml0 */2 * * *DROPPING: 24 of 36 slots in 3 days never got a run
kort0881 facts generatorseveral dailyDROPPING: 17 of 50 missed, median lag 2h39

None of these repos did anything wrong that would explain hours of lag. That is the first question everyone asks, so cronlag answers it explicitly.

Why it is hard to see

The REST API lists schedule runs but never says which cron slot a run was for. A run created at 11:50 for a 0 6 * * * workflow is "a run at 11:50". To get lag you have to expand the cron the way GitHub does (POSIX 5-field, names, the day-of-month/day-of-week OR rule, the 2026 timezone: key with DST gaps), then attribute each run to a slot. And if someone edited the cron last Tuesday, half the "missed" slots are not missed at all.

cronlag reads the workflow file's commit history to know which cron was live when, ties each run to the closest preceding unclaimed slot within 24 hours, and measures lag to run creation. Creation happens before any runner is involved, so that number belongs to GitHub's scheduler. Queue time (creation to start) is shown separately because that one is about your runners.

Is it me or GitHub?

Each workflow gets the checklist those threads walk through by hand: disabled after 60 days of inactivity (warned from day 45), disabled manually, archived repo, YAML that doesn't parse, a cron GitHub rejects, intervals under 5 minutes, minute 0 (with a quieter minute suggested), forks, the schedule not on the default branch, no workflow_dispatch to fall back on, and queueing on runners. If all of that is clean and manual runs start in seconds, it says so: the lag is the scheduler.

Catch-up without double runs

cronlag catchup owner/repo --dispatch, run every 10–15 minutes from any cron that isn't GitHub's, dispatches the latest slot that has no run after a 15 minute grace period, once. When GitHub's late run finally shows up, the Action's mode: guard sees that a dispatch already covered that slot and sets skip=true, so your job doesn't run twice.

npx github:Blackman99/cronlag owner/repo
npx github:Blackman99/cronlag catchup owner/repo --dispatch

Also included: --format markdown to paste into an issue, --fail-on dropped for a weekly health check in CI, and an MCP server (cronlag mcp) so an agent can answer "why didn't my nightly run?" from real data. Or try a repo in the browser; it calls the public API from your machine.

github.com/Blackman99/cronlag · MIT