β ESSAY
While running a cron job with the schedule trigger of GitHub Actions, I ran into executions being delayed by anywhere from dozens of minutes up to two hours. Digging into the cause, it turned out to be a structural limitation rather than a bug, and I eventually moved the job to Firebase Cloud Functions. Here's why this happens, why it's hard to fix, and what alternatives exist when you need on-time execution.
I was running a cron job with the schedule trigger of GitHub Actions.
name: cron
on:
schedule:
- cron: '0 5 * * 1-5'
jobs:
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '12'
check-latest: true
- name: CI
run: |
npm ci
- name: Run Cron
run: |
npm run job
It worked fine at first. But at some point, executions started running 40β50 minutes late, and a job scheduled for 00:00 UTC (09:00 KST) would sometimes only run two hours later.
A job scheduled for 00:00 actually ran at 02:30.
I wasn't the only one experiencing this.
The answer is in GitHub's official documentation.
Note: The
scheduleevent 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.
The docs are terse, so let me elaborate: the delay originates not in the runners but in GitHub's internal job dispatch stage. Delays occur even with self-hosted runners. One case analysis confirms this directly; to summarize:
queued state for 7β8 minutes.online and idle, with runner_id=0 β meaning no runner had been assigned.No matter how fast your runner is, execution can't start until GitHub sends the job over.
On top of this comes the top-of-the-hour congestion problem. The vast majority of repositories schedule their cron at the top of the hour (:00). The docs explicitly state that "high load times include the start of every hour." The dispatch queue floods every hour on the hour, and this delay keeps getting worse as GitHub Actions usage grows. According to community reports, average delays have grown from 9 minutes to 25β30 minutes within a span of months.
And this isn't just a free-plan problem. Nowhere in the official documentation is there any mention of an SLA for schedule execution timing on paid plans (Team, Enterprise). The schedule trigger is best-effort on every plan.
GitHub Actions is a CI/CD platform. Its core value is reacting to code changes, not executing jobs at a fixed time. When the shared runner pool is under load, the question of whether push/PR events or schedule events get resources first answers itself, given the platform's reason for existence.
The fact that the delay occurs in GitHub's internal dispatch layer rather than the runners also makes the problem harder. That's why attaching a self-hosted runner doesn't help. Fixing it would require expanding the dispatch queue's processing capacity or building a dedicated path for scheduled events β changes to GitHub's infrastructure itself.
As an aside: back when I first wrote this post in 2021, the schedule trigger had no timezone support. It only worked in UTC, so you had to convert your cron expressions from Korean time by hand. That problem was solved when the timezone field was added in March 2026.
on:
schedule:
- cron: '30 5 * * 1-5'
timezone: 'Asia/Seoul'
The timezone problem is solved, but the execution-timing accuracy problem remains. Ultimately, GitHub Actions' schedule is only suitable for jobs where "roughly around this time" is good enough. If you need on-time execution, you have to look elsewhere.
At the time, I moved to Firebase Cloud Functions.
exports.cronJob = functions.pubsub
.schedule('0 14 * * 1-5')
.timeZone('Asia/Seoul')
.onRun((_) => {
job()
})
You can set the timezone directly, and execution times were accurate. When you initialize with firebase init, the default directory is ./functions, which you can change in firebase.json.
firebase.json
{
"functions": {
"source": ".",
"runtime": "nodejs12"
}
}
Beyond Firebase, there are a few more options for running cron jobs.
| Platform | Free tier | Timezone | Notes |
|---|---|---|---|
| Firebase Cloud Functions | 2M invocations/month | Supported | Backed by Google Cloud Scheduler. Requires the Blaze (pay-as-you-go) plan, but the free tier is generous |
| Google Cloud Scheduler | 3 free jobs per billing account | Supported | HTTP, Pub/Sub, and App Engine targets. Usable standalone without Firebase |
| Cloudflare Workers Cron Triggers | Within the Workers free plan (shared 100k requests/day) | UTC only | Runs inside Worker code. Practically no cold starts |
| Vercel Cron Jobs | 100 per project on Hobby, once per day | UTC only | Β±59 minutes precision. Suited to low-frequency jobs where accuracy matters less |
Vercel Cron Jobs on the Hobby plan only run once per day with Β±59 minutes precision, so in practice they share the same limitations as GitHub Actions' schedule. If you need on-time accuracy, Google Cloud Scheduler or Cloudflare Cron Triggers are the right fit.