Vivere

Vivere / Answers

GitHub Actions scheduled workflow runs late or is skipped

GitHub documents that scheduled runs can be delayed at busy times and some dropped, and the start of every hour is the busiest. Here is how to measure it in your own repository and what to do when a job cannot be late.

A scheduled workflow set for 0 3 * * * that starts at 03:20 one night and not at all the next is behaving as GitHub documents it. The schedule event is a request to be queued, not a promise of a start time, and the busiest moments on GitHub Actions are exactly the round times most people pick.

What GitHub says

GitHub's documentation for the schedule event, as of October 2026, says three things that matter here:

GitHub does not publish how long a delay can be, and a dropped run is not recorded anywhere. There is no failed run and no skipped run, only a gap.

Measure it from your own run history

Your workflow's run history is the only measurement that applies to you. With the GitHub CLI, list the scheduled runs with the time each one was created:

gh run list --all --workflow nightly.yml --event schedule --limit 50 \
  --json createdAt,conclusion --jq '.[] | "\(.createdAt) \(.conclusion)"'

Compare each createdAt, which is in UTC, with the time the cron line asks for. The difference is the delay; a day, or an hour for an hourly schedule, with no entry is a dropped run. The longest delay you find is what any alert or downstream deadline has to allow for.

Ways to make it less late

Move it off the hour

This is GitHub's own advice and the cheapest fix. 17 3 * * * instead of 0 3 * * *, 41 * * * * instead of 0 * * * *. Any minute other than the top of the hour avoids the busiest moment the documentation names.

Check the timezone and daylight saving

Schedules run in UTC unless the entry sets timezone:

on:
  schedule:
    - cron: "17 6 * * 1-5"
      timezone: "America/New_York"

A schedule written in local time without a timezone is not late, it is an hour or more off, and the offset changes when the clocks do. With a timezone that observes daylight saving, GitHub moves a run that falls in the hour skipped in spring to the next valid time, so a 02:30 schedule runs at 03:00 that night.

Do not ask for more than every 5 minutes

The shortest interval GitHub runs scheduled workflows at is once every 5 minutes. * * * * * does not give you a run every minute, and looks like a workflow that is late most of the time.

Check for concurrency groups

A workflow with a concurrency group allows one run in progress and, by default, one waiting. GitHub's concurrency documentation says a waiting run in the same group is cancelled when a newer one is queued, and cancel-in-progress: true cancels the one that is running. If the scheduled workflow shares a group with a deploy or a push workflow, scheduled runs can be cancelled by unrelated activity; they show as cancelled in the run history rather than as gaps.

When the job cannot be late

If a run has to start within a minute or two of a set time, the schedule event is the wrong tool, because nothing in GitHub's documentation promises that. Two approaches work.

Make lateness harmless

Most scheduled work is about a window: yesterday's orders, last hour's logs. If the job works out its window from the schedule rather than from the clock when it starts, a run that starts 40 minutes late still processes the right hour, and a dropped run can be caught up on the next one. Catching up a missed cron job without getting the dates wrong shows how to key the work by window so late, repeated and caught-up runs are all safe.

Trigger it from a scheduler you control

Add workflow_dispatch to the workflow's triggers and have a scheduler outside GitHub start it through the workflow dispatch API: cron on a server you already run, or a cloud scheduler.

curl -fsS -X POST \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/OWNER/REPO/actions/workflows/nightly.yml/dispatches \
  -d '{"ref":"main"}'

The token needs permission to run Actions on that repository; a classic token needs the repo scope. The start time is then decided by your scheduler rather than by GitHub's schedule queue, though the job still waits for a runner like any other. The workflow file has to be on the default branch for workflow_dispatch to work, and you now have a second scheduler to keep alive, which is worth monitoring in its own right.

Alert on the run that does not come

None of this makes a dropped run visible on its own. To hear about one, have every run report to something outside GitHub and alert when the report is overdue, with a grace period longer than the worst delay in your run history. Getting an alert when a scheduled workflow does not run compares the ways to do it.

In Vivere that is a heartbeat monitor with the workflow's cron expression and timezone, and the Vivere ping action as the job's last step:

      - uses: nicholasbergesen/vivere-ping-action@v1
        if: always()
        with:
          url: ${{ secrets.VIVERE_PING_URL }}
          status: ${{ job.status }}

A late run inside the grace is not an alert; a dropped run, a failure or a run past the grace is. The free plan covers 25 monitors, and the grace period calculator turns the delays you measured into a grace.

Common questions

Why does my GitHub Actions scheduled workflow run late?

GitHub's documentation says the schedule event can be delayed during periods of high load, that the start of every hour is a high-load time, and that if load is high enough some queued jobs may be dropped. It recommends scheduling at a different time of the hour, for example 17 3 * * * rather than 0 3 * * *.

Can a GitHub Actions scheduled run be skipped entirely?

Yes. GitHub documents that some queued scheduled jobs may be dropped when load is high. A dropped run leaves no failed or skipped run behind, only a gap in the workflow's run history, which gh run list --event schedule shows.

How often can a GitHub Actions schedule run?

At most once every 5 minutes. A cron expression asking for every minute does not run every minute.

Find out without looking

Vivere watches for the ping that does not arrive. Add one line to the job, pick where alerts should land, and you hear about the run that never happened.

Start free Read the quickstart

25 monitors, a status page, email and webhook alerts. No card.

Last reviewed October 2026.

Related