Skip to content
Guides

GitHub Actions schedule not running? How to catch missed runs

A scheduled workflow that fails leaves a red run behind. One that never starts leaves nothing at all, and that is the failure people find out about days later. A heartbeat turns the missing run into an alert.

Why scheduled runs go missing

GitHub's own documentation for the schedule event names the causes:

  • Load. “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.” Delayed means late; dropped means the run never happens.
  • Inactivity. In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.
  • The default branch. Scheduled workflows run only on the default branch, from its latest commit. A schedule added on a feature branch, or a workflow file renamed on main, simply stops firing.
  • Daylight saving time. For a schedule with a time zone that observes it, runs in a skipped hour move to the next valid time.

This is not hypothetical. Threads such as “Scheduled GitHub Actions workflows are delayed by several hours or missing completely” and “Scheduled GitHub Actions workflows intermittently delayed or missing” have kept appearing in GitHub's community discussions through 2026.

Why nobody notices

Notifications for workflow runs fire on a run's result. A dropped or disabled schedule produces no run, so there is no result to notify about. The nightly backup, the data export or the certificate renewal just stops, and the first sign is whatever it was protecting going wrong.

Watching for an absence needs something outside GitHub that expects to hear from the job and complains when it does not. That is a heartbeat check, also called a dead man's switch.

Add a heartbeat step

In RealUptime Monitor, add a monitor with Check type set to Heartbeat (cron). Saving shows its ping URL once. Store it as a repository secret named REALUPTIME_PING_URL, because the URL is the credential. Then add two steps at the end of the job:

on:
  schedule:
    - cron: "17 3 * * *"
  workflow_dispatch:

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/nightly.sh
      - name: Heartbeat
        if: success()
        run: curl -fsS --retry 3 "${{ secrets.REALUPTIME_PING_URL }}"
      - name: Report failure
        if: failure()
        run: curl -fsS --retry 3 "${{ secrets.REALUPTIME_PING_URL }}/fail"

The first step runs only when everything before it succeeded, so a broken run never sends an all-clear. The second sends a fail ping, which marks the monitor down at once with “run failed” wording instead of waiting for the deadline. If the run never starts, neither step runs, and the monitor goes down when the deadline passes. The cron minute above is 17 rather than 0 because GitHub recommends avoiding the top of the hour.

Heartbeats are on every plan, Free included, and count as one of your monitors. For run durations and stuck runs, add a start ping as described in reporting failed and stuck cron runs.

Pick the interval and grace

Set the expected interval to the schedule's period: 86,400 seconds for a daily job, 3,600 for an hourly one. Then set a grace period that covers GitHub's normal lateness plus your job's runtime. For a daily job, an hour or two of grace keeps a late start from paging anyone while still catching a skipped day the same morning.

The longest interval a heartbeat accepts is 24 hours, so a weekly schedule needs a different approach, for example a small daily workflow whose only job is to ping.

Other schedulers

The same pattern, written for each scheduler's own syntax:

The account that holds the heartbeat also includes Status for a public status page, Errors for the exception a job throws, and Outages for checking whether GitHub itself is having a bad day.

Sources

All read on November 1, 2026.