GitHub logo

GitHub Actions cron: why scheduled workflows run late (and how to fix it)

The schedule trigger is the quickest way to run a GitHub Actions workflow on a timer: add a cron expression to a YAML file, push, and you are done. Then, a few days later, you notice the nightly job started 40 minutes late, skipped a run, or quietly stopped running altogether.

None of this is a bug in your workflow. Scheduled workflows are best-effort by design. This guide explains how on: schedule really behaves, the most common reasons a scheduled workflow is late or missing, and how to start a workflow on time when timing matters.

Short answer

GitHub can delay scheduled runs when Actions is busy, especially at the start of every hour, and can drop queued runs under heavy load. Avoid minute 0, never rely on exact timing, and if a run must happen on time, add a workflow_dispatch trigger and call it from an external scheduler such as Cronit.

How on: schedule works

You add one or more cron expressions under on.schedule. Since March 2026 each entry can also take an optional timezone with an IANA name; without it, the expression is evaluated in UTC.

name: Nightly report

on:
  schedule:
    - cron: '17 3 * * *'          # 03:17 every day
      timezone: 'Europe/Paris'    # optional, UTC if omitted
  workflow_dispatch:              # adds a "Run workflow" button

jobs:
  report:
    runs-on: ubuntu-latest
    concurrency:
      group: nightly-report       # never two reports at once
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v5
      - run: ./scripts/build-report.sh

A few rules apply to every scheduled workflow:

  • The shortest interval is 5 minutes. An expression such as * * * * * will not run every minute.
  • Only the default branch counts. The workflow file must exist on the default branch, and scheduled runs use the latest commit on that branch. A schedule added on a feature branch does nothing until it is merged.
  • The syntax is standard five-field cron. If you are unsure what an expression means, paste it into the cron analyzer or build it with the crontab generator.

When a workflow has several schedules, github.event.schedule contains the expression that triggered the current run, so you can branch on it:

on:
  schedule:
    - cron: '17 3 * * *'
    - cron: '45 6 * * 1'

jobs:
  weekly-only:
    if: github.event.schedule == '45 6 * * 1'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Monday run"

Why your scheduled workflow is late or missing

1. GitHub is busy, and the start of the hour is the busiest time

GitHub's documentation is explicit: the schedule event "can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour. If the load is sufficiently high enough, some queued jobs may be dropped."

Thousands of repositories use 0 * * * * or 0 0 * * *, so those minutes are the most crowded. Moving your job to an odd minute, such as 17 or 43, is the cheapest improvement you can make.

2. The workflow was disabled after 60 days

In a public repository, scheduled workflows are automatically disabled when there has been no repository activity for 60 days. The workflow then shows as disabled in the Actions tab. Re-enable it there, or from the command line:

gh workflow enable nightly.yml

Avoid "keep-alive" commits that exist only to reset the timer. The rule applies to workflows with a schedule trigger; a workflow started only through workflow_dispatch has no schedule to disable, which is one more reason to trigger important jobs from outside (see below).

3. The workflow file is not on the default branch

Check which branch is the default in the repository settings. Renaming master to main, or changing the default branch, can silently move your schedule to a branch that does not contain the workflow file.

4. The time zone is not what you think

Without a timezone key, 0 9 * * * means 09:00 UTC, which is 10:00 or 11:00 in Paris and 04:00 or 05:00 in New York depending on the season. If you add a time zone, remember daylight saving time: GitHub moves a run that falls in a skipped hour to the next valid time, so a 02:30 schedule runs at 03:00 on the spring-forward night. Our guide to cron and daylight saving time covers the details.

5. The expression is invalid or more frequent than allowed

If the YAML is invalid, the Actions tab shows an error for the workflow file and nothing is scheduled. Expressions that ask for runs more often than every 5 minutes do not run at that rate.

6. The run is queued, not late

Scheduling only creates the run. It still waits for a runner like any other job, and in private repositories it uses your Actions minutes. If you use self-hosted runners that are offline, the run sits in the queue.

Start the workflow on time with workflow_dispatch

A workflow_dispatch run is created the moment someone calls the GitHub API, so it skips the scheduling queue entirely. Pair it with an external scheduler and you keep GitHub's runners while getting predictable start times, run logs, and no 60-day expiry.

Diagram comparing on: schedule, which goes through GitHub's shared queue and can start late, with workflow_dispatch triggered by Cronit through the REST API.
With on: schedule, runs wait in GitHub's shared queue. With workflow_dispatch, the run is created when your scheduler calls the API.

Step 1: add the trigger

Add workflow_dispatch to the workflow, with inputs if you want to pass parameters. Once the external trigger works, remove the schedule entry: otherwise the job runs twice, and in a public repository the 60-day inactivity rule can still disable the workflow.

on:
  workflow_dispatch:
    inputs:
      reason:
        description: 'Why this run was started'
        required: false
        default: 'manual'

Step 2: create a token with the smallest possible scope

Create a fine-grained personal access token that only has access to this repository, with the repository permission Actions: Read and write. Give it an expiry date and put a reminder in your calendar to rotate it.

Step 3: test the call with curl

curl -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  https://api.github.com/repos/OWNER/REPO/actions/workflows/nightly.yml/dispatches \
  -d '{"ref":"main","inputs":{"reason":"cronit"}}'

Replace OWNER, REPO and the workflow file name. ref is the branch or tag to run on. With API version 2026-03-10, a successful call returns 200 with the new run's workflow_run_id and URLs. With the older 2022-11-28 version, it returns 204 with an empty body unless you add "return_run_details": true.

Step 4: schedule the call in Cronit

  1. In Cronit, create a new recurring job and give it a name such as "Nightly report (GitHub)".
  2. Set the method to POST and the URL to the …/dispatches address from step 3.
  3. In Headers, add Accept: application/vnd.github+json, Authorization: Bearer YOUR_TOKEN and X-GitHub-Api-Version: 2026-03-10.
  4. In Payload, enter {"ref":"main","inputs":{"reason":"cronit"}}.
  5. Under triggering, choose a cron expression, for example 17 3 * * *.

Every call then appears in the job's execution logs with its date and status, so a failed dispatch (an expired token, for example) is visible immediately rather than discovered weeks later.

Organization tip: GitHub's workflow execution protections (in public preview since June 2026) let admins restrict which actors can trigger workflows. If your organization uses them, make sure the account that owns the token is allowed to run workflow_dispatch.

Which trigger should you use?

on: scheduleworkflow_dispatch + Cronit
Start timeBest effort; can be late or dropped under loadWhen the API is called
Shortest interval5 minutesSet by your scheduler
Time zoneUTC, or one IANA zone per entrySet by your scheduler
Public repo inactive 60 daysWorkflow is disabledNot affected when there is no schedule trigger
SetupA few lines of YAMLYAML + token + scheduled HTTP call
Run historyActions tabActions tab + scheduler logs

For a cleanup job where a delay of 30 minutes does not matter, on: schedule on an odd minute is perfectly fine. For reports people wait for, deployments, data syncs and anything with a deadline, trigger it explicitly.

FAQ

What is the minimum interval for GitHub Actions cron?
Every 5 minutes. Shorter intervals are not supported for scheduled workflows.
What time zone does GitHub Actions cron use?
UTC by default. Since March 2026 you can add a timezone key with an IANA name, such as America/New_York, to each schedule entry.
Why is my scheduled workflow not running at all?
The usual causes are: the workflow file is not on the default branch, the workflow was disabled after 60 days without activity in a public repository, or the YAML is invalid. Check the Actions tab for the workflow's status.
How do I run a scheduled workflow manually?
Add workflow_dispatch: to the on section. You then get a "Run workflow" button in the Actions tab, and you can run gh workflow run nightly.yml --ref main from the command line.

Run online cronjobs without server setup

Plan, schedule and run periodic tasks and scripts, from your browser with the first online cronjob platform.

Try for free