GitHub Actions monitoring
GitHub shows you the runs that failed but never the runs that did not start, so watch a scheduled workflow from outside: the last step reports success, and a monitor alerts you when that report goes stale.
The Actions tab is a good record of what ran and no record of what did not. When a scheduled workflow fails, GitHub emails the user who last modified the cron line in the workflow file, which may be someone who left in March. When the workflow never starts, nobody hears anything, because there is no run to fail.
GitHub Actions scheduled workflow monitoring
Three behaviours from GitHub's own docs make a schedule less reliable than the YAML suggests:
- In a public repository, scheduled workflows are disabled after 60 days with no repository activity. The nightly backup of a finished side project is exactly the workflow this hits.
- The schedule event can be delayed during high load, and the start of every hour counts as high load. Under enough load, queued runs are dropped, not just delayed.
- Schedules run only on the default branch, no more often than every 5 minutes, and in UTC unless the entry sets the timezone key GitHub added in March 2026.
So the question to monitor is not "did a run fail". It is "did a successful run happen recently enough".
GitHub Actions cron: report in on success
Put the report at the end of the job. Steps stop at the first failure, so the last step only runs when everything before it passed. The odd minute keeps the run off the top of the hour.
name: nightly-sync
on:
schedule:
- cron: '17 3 * * *' # 03:17 UTC
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- run: ./scripts/sync.sh
- name: Report success
run: >
curl -fsS -m 10 -X POST
-H "Authorization: Bearer ${{ secrets.HEARTBEAT_TOKEN }}"
https://yourapp.com/internal/heartbeat/nightly-syncWhy the check lives in your app
A Logdash push monitor looks like the obvious target and is the wrong one here. Push monitors are a Pro feature, they expect a ping inside every 15-second window, and they flip to down on the first empty one. There is no grace setting. A workflow that runs every 5 minutes at best would flap after every run. So turn it around: the workflow tells your app, your app keeps the mark for 26 hours, and an ordinary HTTP monitor asks whether the mark is still there. That runs on the free plan. The 26 hours is a daily schedule plus two hours of slack for GitHub delays; set it to your own interval plus the lateness you can live with.
import express from 'express';
import { Redis } from 'ioredis';
const app = express();
const redis = new Redis(process.env.REDIS_URL!);
const KEY = 'heartbeat:nightly-sync';
const TTL = 26 * 60 * 60; // one day plus slack for GitHub delays
app.post('/internal/heartbeat/nightly-sync', async (req, res) => {
if (req.get('authorization') !== `Bearer ${process.env.HEARTBEAT_TOKEN}`) {
res.sendStatus(401);
return;
}
await redis.set(KEY, Date.now(), 'EX', TTL);
res.sendStatus(204);
});
// Logdash checks this. 503 means no successful run in 26 hours.
app.get('/health/nightly-sync', async (_req, res) => {
res.sendStatus((await redis.exists(KEY)) ? 200 : 503);
});
app.listen(3000);- Ship the two routes Deploy them, add HEARTBEAT_TOKEN as a repository secret, then run the workflow once by hand so the key exists before the first check.
- Point a monitor at it Create a service in Logdash with an HTTP monitor on /health/nightly-sync and connect a Telegram channel. Checks run every 5 minutes on the free plan and every minute on Builder.
- Let it go stale Delete the key in Redis. The route answers 503, the monitor flips to down on the next check, and a Telegram alert names the monitor and the 503.
Logdash vs Healthchecks.io for scheduled workflows
| Feature | Logdash | Healthchecks.io |
|---|---|---|
| Code in your app | Two routes and a Redis key | None, the workflow curls their URL |
| Schedule awareness | Expressed as the key TTL | Cron expression, time zone and grace time per check |
| Catches the 60-day disable | Yes, the key expires | Yes, the ping stops |
| HTTP checks on the app the workflow feeds | Same service, with response time | Not offered, it only listens for pings |
| Free plan | Five services, checks every 5 minutes | 20 checks |
| Alert channels | Telegram and webhook | Email, Slack, Telegram and many more |
When Healthchecks.io is the better pick
- You do not want to add routes to an app just to watch a workflow. A curl to their ping URL is the whole integration.
- You run many scheduled workflows. One check per workflow with its own cron expression beats one route per workflow.
- The workflow does not touch an app you run, for example it cuts a release or prunes a bucket.
- You want email or Slack alerts. Logdash sends Telegram and webhooks only.