A Scheduler That Does Not Promise a Schedule
You add a schedule block, set it to 0 6 * * *, and assume the job runs at 06:00 UTC. Most days it roughly does. Then one morning the run lands at 14:30. Another morning it does not land at all, and nothing in the Actions tab explains why. From GitHub's side nothing failed. The run was queued, the queue was busy, and the job waited.
You cannot file this as a bug. GitHub's own docs say the schedule event "can be delayed during periods of high loads of GitHub Actions workflow runs," that "high load times include the start of every hour," and that "if the load is sufficiently high enough, some queued jobs may be dropped." A dropped job produces no run, no log, no notification. The schedule is a request, not a guarantee.
The Delays Got Worse Through 2026
The gap between "occasionally late" and "unusable for anything time-sensitive" widened over the year, and people tracked it in the GitHub community discussions.
A Nightly Job Drifting Toward Four Hours
One thread from May 2026 logged a nightly workflow set for 00:40 UTC. The author had a year of timestamps: an average delay near 1 hour 40 minutes in 2025, up to 4 hours 30 minutes by that May. The drift was not noise. It grew, month over month, on the same workflow that used to run close to schedule.
Hours Late, Then Dropped Entirely
A July 2026 thread was uglier. A daily job fired 14.5 hours late on the 6th, 12.5 hours late on the 7th, did not run at all on the 8th, and stayed in the 8-to-12-hour band for the rest of the week. Moving the cron minute changed nothing. Underneath both threads sits the usual row of "same here" replies, each with its own timestamps.
Why the Top of the Hour Is the Worst Slot
The cause is shared, multi-tenant capacity. Every scheduled workflow on GitHub competes for the same runner pool at the same instants, and the popular instants are round numbers: the top of the hour, midnight UTC. A line of 0 * * * * asks to run at the moment the largest number of other repos also asked to run.
GitHub's advice is to step away from those peaks:
on:
schedule:
# Avoid the top of the hour, pick an odd minute
- cron: "23 6 * * *"
Minute 23 or 47 helps on a normal day, because you leave the thundering herd. It does nothing on a bad day, when the backlog is measured in hours and a seven-minute head start is noise. The July 2026 reports came from people who had already moved off :00.
Dropped Runs Are Not Late Runs
A late run is annoying. A dropped run is dangerous, because the two look identical until the deadline passes. There is no "missed" state in the Actions UI. A run that never gets scheduled simply is not there. If your workflow renews a certificate, rotates a token, or publishes a nightly build, the absence of the run is the failure, and you hear about it from whatever breaks next.
It is the same trap as a cron job that stops firing, with one twist: on your own host you can at least grep the logs and confirm cron tried. On GitHub Actions there is nothing to grep.
Public Repos Get Switched Off Silently
There is a second way a scheduled workflow dies quietly. In a public repository, GitHub disables scheduled workflows after 60 days without repository activity. A quiet utility repo, say a status-page updater or a mirror sync, can cross the two-month mark with no commits. Its schedule switches off, and no email goes out. The job comes back only when someone pushes or re-enables it by hand.
What You Can and Cannot Fix
Move Off the Peak, but Do Not Trust It
Pick an odd minute and stay away from 0 and 30. It trims the routine delay. Treat it as a small optimisation and never hang a hard deadline on it.
Use a Real Scheduler for Hard Deadlines
Some jobs must run at a specific time: a market-open task, an end-of-day close, a billing cutoff. For those, GitHub Actions is the wrong trigger. Run the schedule somewhere with an SLA, such as a cloud scheduler or a systemd timer on a host you control, and expose the workflow through workflow_dispatch so that scheduler can call it through the API at the right moment. GitHub still does the work. Something else owns the clock.
on:
workflow_dispatch:
# keep a loose cron as a fallback, not the primary trigger
schedule:
- cron: "23 6 * * *"
Stop Reading Silence as Success
GitHub emails you when a workflow fails. It does not email you when a workflow never starts. A quiet inbox tells you nothing about the runs that were dropped, and that single distinction is the whole problem.
Monitor for the Missing Run
The failure mode here is absence, so the only monitoring that catches it watches for the absence of success. Have the workflow check in at the end of a successful run, and have something outside GitHub expect that check-in. A run four hours late produces a check-in four hours late, and you see the drift. A run that gets dropped produces no check-in, and you get paged, without GitHub ever having to report a thing.
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- name: Do the actual work
run: ./scripts/nightly-sync.sh
- name: Check in
run: curl -fsS --retry 3 https://cronguard.app/api/ping/your-monitor-id
Put the check-in as the last step, so it only fires when everything before it succeeded. Set the monitor's period to your real schedule plus a grace window wide enough to swallow the normal drift. An ordinary late run then stays quiet, and a genuinely missing one wakes you up.
Frequently asked questions about GitHub Actions scheduled workflows
Why did my scheduled workflow run hours late? GitHub Actions runs scheduled workflows on shared capacity, and the schedule event is delayed during high load. The start of every hour is a documented high-load window, so a job set for that slot waits behind everyone else's. Through 2026 users reported the delay growing from under two hours to four hours and more.
Can a scheduled workflow be skipped entirely? Yes. GitHub's documentation says that if load is high enough, some queued jobs may be dropped. A dropped run leaves no entry in the Actions tab, no log, and no failure email, so it looks identical to a run that has simply not started yet.
Does changing the cron minute fix the delay? It helps on a normal day and not at all on a bad one. Moving off minute zero to something like 23 or 47 steps you out of the busiest slot and shaves routine delay. When the backlog runs to hours, a few minutes of offset make no difference, which is what people who already left the top of the hour keep reporting.
Why did my scheduled workflow stop running on its own? In a public repository, GitHub disables scheduled workflows after 60 days without repository activity. A low-traffic repo can pass that line with no commits, its schedule switches off silently, and it resumes only once someone pushes a change or re-enables it by hand.
How do I get alerted when a scheduled workflow does not run? Add a check-in as the final step of the job and point it at an external monitor that expects it on your real schedule. A late run makes the check-in late and you see the drift; a dropped run means the check-in never arrives and the monitor alerts you. This watches for the absence of success, the only signal that catches a run GitHub never started.
Further reading
- Everything Runs at Midnight: Spreading Out Your Scheduled Jobs
- Dead Man's Switch Monitoring: The Only Reliable Way to Watch Cron Jobs
- Cron Alternatives Compared: systemd Timers, Celery Beat, and More
Conclusion: GitHub Actions schedules are best-effort, and in 2026 that "best" drifted to hours late with runs dropped outright. You cannot fix the shared queue. You can stop trusting it blindly: move off the top of the hour, hand any hard deadline to a scheduler with an SLA, and monitor for the absence of a successful run so a dropped job pages you instead of surfacing as the next thing that breaks.