DEV Community

Hira for LayerCall

Posted on

GitHub Actions ran my hourly cron once in ten hours

I moved a production audit onto a GitHub Actions schedule — 25 * * * *, every hour — and after a day it had fired zero times. Not late. Zero.

I assumed I had the cron syntax wrong. I did not. Here is what a day on the same repository looked like, counted from the Actions API:

workflow        cron            slots   runs
audit           25 * * * *        24      0
mailbox probe   40 * * * *        24      2
nightly backup  40 2 * * *         1      1
job watch       20 7 * * *         1      1
Enter fullscreen mode Exit fullscreen mode

Both hourly schedules delivered about 4% of their slots. Both daily ones delivered 100%. Same repo, same day, same permissions.

This is documented, once you know to look

Scheduled workflows are best-effort, and high-frequency ones on private repositories get de-prioritised and dropped under load.

What is not documented is that nothing tells you. The workflow page shows the runs that happened. It does not show the twenty-two that did not. There is no "skipped" entry, no warning, no difference between "this hour was quiet" and "this hour never ran."

Why it matters more than a missed job

The thing that did not run was the audit — the check whose entire purpose is to be what notices.

A watchdog that fires one hour in ten, silently, is worse than no watchdog. With no watchdog you know you are not being watched. With this one you believe you are, and the belief is wrong 90% of the time. You stop looking, because something else is looking for you.

What I did

The audit now runs from the platform cron on the app's host. On the same day, measured against the same clock, that scheduler landed every quarter-hour slot within seven seconds:

15:00:00   14:45:01   14:30:07
14:15:02   14:00:03   13:45:02
Enter fullscreen mode Exit fullscreen mode

The GitHub workflow keeps only its manual trigger — and that is worth keeping, because run by hand it grades production from outside the deployment, which the platform cron cannot do for itself.

The job that could not move

One of them needs outbound port 25, which serverless blocks. That is the whole reason it lives in CI in the first place, so it could not follow the audit.

It dropped from hourly to four fixed daily times, on the theory that daily is what was measured to work. Four a day is not proven — it sits between the two measurements I actually have, and I would rather say that than imply otherwise.

So the backstop is that the platform side now raises an alarm when anything has waited over twelve hours, which is deliberately before the twenty-four-hour point where the queue deletes it. The alarm has to arrive while the rows can still be drained, not announce data already dropped.

If those four slots get dropped too, that alarm is what will say so — instead of another silent week.

If you run anything hourly on Actions

Count the runs against the slots. It is one API call:

gh run list --workflow=your-workflow.yml --limit 100 \
  --json event,createdAt --jq '[.[] | select(.event=="schedule")] | length'
Enter fullscreen mode Exit fullscreen mode

Compare that to how many slots have passed since the schedule went live. The number surprised me, and it had been wrong for a day before anyone thought to check.

Top comments (0)