Troubleshooting · GitHub Actions

GitHub Actions cron schedule not running? Every silent cause

Why a GitHub Actions cron schedule isn't running or triggering — wrong branch, the 60-day auto-disable, UTC-only timing, load delays, dropped runs, spending limits — and the fix for each.

on: schedule is the most deceptively unreliable trigger in GitHub Actions. It fails for reasons that produce no error anywhere — no red X, no email, no log. This guide walks every known cause, roughly in order of likelihood.

30-second diagnosis:

SymptomMost likely causeJump to
Never ran, not even onceFile not on the default branch§1
Ran for weeks/months, then stopped60-day auto-disable (public repos)§2
Runs, but 10–40 minutes lateBest-effort queue delays§3
Runs at the “wrong” timeCron is UTC-only§4
Occasionally skips a runDropped under load§3
Stopped after a billing changeSpending limit (private repos)§5

1. Is the workflow file on the default branch?

Scheduled workflows only run from the default branch (usually main). A workflow added on a feature branch, or merged into anything other than the default branch, will never fire — no matter how correct the cron expression is. This also means a schedule you’re iterating on in a PR won’t run until the PR is merged.

# the file must exist here:
git show origin/main:.github/workflows/nightly.yml

If you’re testing on a branch, add a workflow_dispatch: trigger alongside the schedule so you can run it manually until it’s merged:

on:
  schedule:
    - cron: "17 3 * * *"
  workflow_dispatch: {}     # manual runs while iterating

One more subtlety: the schedule that runs is the one defined on the default branch. Editing the cron expression on a feature branch changes nothing until merge.

2. Did GitHub auto-disable it after 60 days of inactivity?

In public repositories, GitHub automatically disables scheduled workflows when the repository has had no activity for 60 days. This is the classic “it ran fine for months, then silently stopped” case — and it disproportionately hits exactly the repos that rely on schedules: scrapers, backup jobs, data pipelines that otherwise receive no commits.

Check the Actions tab → select the workflow. If it was disabled you’ll see a banner: “This scheduled workflow is disabled because there hasn’t been activity in this repository for at least 60 days” — with an Enable workflow button. GitHub also emails the workflow author before disabling, but that mail is easy to lose.

Re-enabling fixes it until the next 60 quiet days. For a naturally low-activity repo you have three durable options:

  1. Real activity — any commit to the repo resets the clock.
  2. A keep-alive step — some workflows commit a timestamp file or use a keep-alive action to generate synthetic activity. It works, but you’re polluting history to satisfy a scheduler.
  3. Move the trigger out of GitHub — keep the job as workflow_dispatch and have an external scheduler call the GitHub API on a real schedule (see the last section). External API triggers count as activity and are exact-time.

3. Expect delays — and dropped runs

GitHub runs scheduled workflows on a best-effort queue, and the official docs are explicit that scheduled events can be delayed during periods of high load, and delayed runs can be dropped entirely. Two practical consequences:

  • Delays of 5–30+ minutes are normal, worst at the top of the hour when everyone’s 0 * * * * jobs pile up. Schedule at an odd minute (17 * * * *, 43 3 * * *) to dodge the rush — it reliably shaves the worst of the delay, though it can’t eliminate it.
  • Under high load, runs are skipped with no retry and no notification. If you process “everything since the last run”, write your job to tolerate a missing interval (e.g. derive the window from the last successful run, not from “now minus one hour”).

Also note the floor: schedules can’t run more often than every 5 minutes, and in practice even 5-minute schedules land irregularly.

You can measure your actual delay: compare the workflow run’s created_at with the scheduled minute —

gh run list --workflow nightly.yml --json createdAt,displayTitle --limit 10

If your use case needs minute-accurate execution (billing, SLA reports, time-sensitive notifications), GitHub’s scheduler is the wrong tool — see the GitHub Actions cron comparison for what an exact-time scheduler changes.

4. The cron expression is UTC — always

GitHub Actions has no timezone setting for schedules. 0 9 * * * is 09:00 UTC:

You wroteUTCBerlin (winter)Berlin (summer)
0 9 * * *09:0010:0011:00
30 4 * * 1Mon 04:30Mon 05:30Mon 06:30

Your “9 AM job” moves by an hour twice a year in local terms. There is no workaround inside GitHub short of maintaining two workflows and toggling them at DST boundaries — or triggering externally from a timezone-aware scheduler.

The expression itself is standard 5-field cron (no seconds field, no @daily aliases). Keep the quotes — a bare * can trip YAML parsing:

on:
  schedule:
    - cron: "*/15 * * * *"   # every 15 minutes (UTC)
    - cron: "0 3 * * 1-5"    # 03:00 UTC on weekdays — multiple entries are fine

If you’re unsure what an expression means, paste it into the cron expression explainer or browse the common schedule reference.

5. Other silent kill switches

  • Manually disabled — someone clicked “Disable workflow” in the Actions tab. The workflow file still exists, everything looks normal, nothing runs.
  • Spending limit reached — for private repos, if the Actions minutes budget is exhausted or a payment failed, all runs stop until the next billing cycle.
  • Forks — scheduled workflows are disabled by default in forks. If you forked a repo that has a schedule, it won’t run until you enable it.
  • Repository archived — archiving stops all workflows.
  • Organization or enterprise policy — an admin can disable Actions, restrict it to selected repositories, or block third-party actions your workflow needs.
  • The previous run is still in progress — schedules don’t queue behind a running instance by default; combined with a long-running job you can see “missing” runs that were actually skipped by concurrency.

Quick way to check a workflow’s real state via the API:

gh api repos/OWNER/REPO/actions/workflows --jq '.workflows[] | {name, state, path}'

state will read disabled_inactivity, disabled_manually, or active — which distinguishes §2 from §5 instantly.

The deeper problem: nobody tells you when the schedule stops

Every failure mode above is silent. GitHub notifies the workflow author on a failed run — but a run that never starts produces nothing to notify about.

The robust pattern is a dead-man’s switch: the workflow pings a heartbeat URL on success, and an external monitor alerts you when the ping goes missing — whatever the reason:

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - run: ./run-the-actual-job.sh
      - name: Report success
        run: curl -fsS https://ping.steadycron.com/${{ secrets.STEADYCRON_PING_TOKEN }}

Now a disabled workflow, a dropped run, a bad deploy, or a 60-day auto-disable all produce the same result: a missed ping and an alert to Slack or email within your grace period. Setup details in heartbeat monitoring.

And if the timing itself matters (billing runs, reports), don’t rely on GitHub’s best-effort scheduler at all — keep the job as workflow_dispatch and trigger it via the GitHub API from a scheduler with exact timing and retries:

# SteadyCron HTTP job → GitHub API → your workflow, on time, with retries
method: POST
url: https://api.github.com/repos/OWNER/REPO/actions/workflows/nightly.yml/dispatches
headers:
  Authorization: Bearer ${GITHUB_PAT}
  Accept: application/vnd.github+json
body: '{"ref":"main"}'

This kills three problems at once: exact timing with retries, timezone-aware schedules, and — because API triggers count as repository activity — no more 60-day auto-disable.

Frequently asked questions

Why is my GitHub Actions scheduled workflow not running at all?

The two most common causes: the workflow file isn't on the default branch (schedules only fire from the default branch), or GitHub auto-disabled it after 60 days without repository activity in a public repo. Check the Actions tab for a disabled-workflow banner, and confirm the file exists on main.

What timezone does GitHub Actions cron use?

Always UTC. There is no timezone setting for on: schedule0 9 * * * means 09:00 UTC, which is 10:00 or 11:00 in Berlin depending on daylight saving time. Your job's local time shifts by an hour twice a year.

Why is my GitHub Actions cron delayed by 15–30 minutes?

Scheduled workflows run on a best-effort queue. Delays of 5–30+ minutes are normal, worst at the top of the hour when everyone's 0 * * * * jobs pile up. Scheduling at an odd minute (e.g. 17 * * * *) reduces but doesn't eliminate the delay — and under high load, runs can be dropped entirely.

How often can a GitHub Actions schedule run?

At most every 5 minutes — */5 * * * * is the floor. Anything more frequent is silently coerced. In practice, queue delays make even 5-minute schedules irregular.

Does GitHub notify me when a scheduled run is skipped or the workflow is disabled?

No notification is sent for a dropped run — a run that never starts produces nothing to notify about. For the 60-day auto-disable you get a banner in the Actions tab (and an email to the workflow author), which is easy to miss. The reliable fix is an external dead-man's switch that alerts when the expected run doesn't happen.