All Articles
CategoryDevOps
Reading Time
7 min read
Published
2026-10-15
Word Count
1,336words

Grab a coffee — this one is a deep dive!

An uptime check on GitHub Actions, and what a five-minute cron really delivers

Summary

site-uptime probes muhittincamdali.com from GitHub Actions and alerts through an issue. Run history shows how often a */5 schedule actually ran.

  • The repository's README states the reason it exists in two sentences.
  • When both attempts fail, the workflow looks for an open issue with the label outage.
  • The first failed run came at 07:34 UTC on 28 September, and issue #1 was opened a minute later.
  • Source code: [github.com/muhittincamdali/site-uptime](https://github.com/muhittincamdali/site-uptime)
An uptime check on GitHub Actions, and what a five-minute cron really delivers

Table of Contents


The problem

The repository's README states the reason it exists in two sentences. On 23 July 2026

muhittincamdali.com went down and stayed down for about three and a half days, because all of its

monitoring ran on the same server as the site. When the server could not be reached, neither could

anything that would have reported it.

The fix is a check that lives somewhere else. site-uptime runs entirely on GitHub Actions: no

server, no secret beyond the token every workflow gets, and no account with a monitoring service.

The first commit is from 3 August 2026; the repository has three commits and two workflows, about

110 lines of YAML in total.

How it works

uptime.yml is scheduled with cron: "*/5 * * * *" and can also be started by hand. Each run

requests two URLs, the home page and /api/health/, and treats the site as up only if both return

HTTP 200. A failure is retried once after 30 seconds before it counts:

sh
1probe() {
2 HOME_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 20 "$SITE/" || echo 000)
3 HEALTH_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 20 "$SITE/api/health/" || echo 000)
4 echo "home=$HOME_CODE health=$HEALTH_CODE"
5 [ "$HOME_CODE" = "200" ] && [ "$HEALTH_CODE" = "200" ]
6}

Checking both URLs separates two failures. The home page can be served while the application

behind it is broken; the health endpoint covers that. -m 20 caps each request at 20 seconds, and

|| echo 000 turns a connection failure into a status code the comparison can handle.

Design decisions and the alternatives

An issue as the alert and as the state. When both attempts fail, the workflow looks for an

open issue with the label outage. If there is none, it opens one with the time, the URLs it

checked and a short runbook; if there is one, it adds a "still down" comment. When the site answers

again, it comments and closes the issue:

sh
1EXISTING=$(gh issue list --repo "$REPO" --state open --label outage --json number --jq '.[0].number // empty')
2if [ -n "$EXISTING" ]; then
3 NOW=$(date -u +"%Y-%m-%d %H:%M UTC")
4 gh issue comment "$EXISTING" --repo "$REPO" --body "✅ RECOVERED at $NOW — site and health endpoint both 200."
5 gh issue close "$EXISTING" --repo "$REPO"
6fi

The issue title carries the time of the first failure, and the runbook in the body lists where to

look first: the hosting account, the server's network status and the application service, plus a

pointer to the July incident. A run can also be started by hand from the Actions tab

(*Site Uptime Check* → *Run workflow*), which is the quickest way to confirm a recovery.

This gives three things without any extra storage. Only one alert is opened per outage, because

the open issue is the record that an outage is in progress. Delivery is GitHub's: the README notes

that a new issue sends the owner an email. And the issue's comments become the outage log, with a

timestamp for every failed run.

The alternatives were a hosted monitoring service or a small check on a second machine. Both work,

but both add something to maintain: an account, a host, credentials. The whole point here was a

check that does not share anything with the server it watches; GitHub is already where the code

lives.

The smallest permissions that work. The workflow declares issues: write and

contents: read and uses only github.token. A failed run also ends with exit 1, so the Actions

tab shows red runs during an outage, not only the issue.

No overlapping runs. concurrency: group: uptime with cancel-in-progress: false queues a run

behind one that is still retrying instead of cancelling it. Two runs racing could otherwise both see

"no open issue" and open two.

What the run history shows

The schedule says every five minutes. That would be 288 runs a day. GitHub's documentation is

careful about this: scheduled runs can be delayed when Actions is under high load, and some queued

runs may be dropped. The repository's own run history shows how much that matters.

As of 15 October 2026 the uptime workflow had 1,082 runs in total since 3 August. For the last

200 runs, the gaps between scheduled runs were:

Period
Scheduled runs
Median gap
Shortest
Longest
8–19 September 2026
83
184 min
100 min
355 min
20 September–2 October
73
248 min
104 min
498 min
5–15 October 2026
43
319 min
151 min
585 min

So the check runs every few hours, not every five minutes. For this site that is still the

difference that matters: the July outage went unnoticed for days, while the next one was reported by

the first run that saw it, even if that run came hours after the site went down.

A real outage, and a monitor that switched itself off

28–30 September 2026. The first failed run came at 07:34 UTC on 28 September, and issue #1 was

opened a minute later. Over the next two and a half days ten more runs failed, each adding a

"still down" comment. The run at 18:36 UTC on 30 September saw both URLs return 200, commented and

closed the issue. The issue and its comments are the whole record; the repository does not say what

caused the outage, so neither does this article.

2 October 2026. Two days later GitHub disabled the "Site Uptime Check" workflow. GitHub turns

off scheduled workflows in public repositories after 60 days without repository activity, and the

only commit was from 3 August. The scheduled runs themselves, including the ones that commented on

issue #1, did not keep it alive. A

monitor that silently stops is worse than none, because it still looks set up.

The fix, committed on 5 October, is a second workflow that makes a commit once a month. It writes a

timestamp to .github/keepalive through the contents API:

sh
1FILE=.github/keepalive
2STAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
3SHA=$(gh api "repos/$GITHUB_REPOSITORY/contents/$FILE" --jq .sha 2>/dev/null || true)
4gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$FILE" \
5 -f message="chore: keepalive $STAMP" \
6 -f content="$(printf '%s\n' "$STAMP" | base64)" \
7 ${SHA:+-f sha="$SHA"} > /dev/null

${SHA:+-f sha="$SHA"} passes the existing file's SHA only when there is one, so the first run

creates the file and later runs update it. The workflow needs contents: write and uses no

third-party action. Its comment notes one more detail: a commit made with GITHUB_TOKEN does not

start other workflows, so the keepalive cannot trigger a cascade of runs. The first keepalive commit

was made by a manual run the same evening; the schedule (17 6 1 * *) takes over from 1 November.

Limits

  • Resolution is hours, not minutes. The measured gaps above are what to expect. A short outage

between two runs is not seen at all.

  • It depends on GitHub. If GitHub Actions or issue notifications are down, so is the alert.

That is acceptable here because the point is to be independent of the site's server, not of

everything.

  • One location, two status codes. The check runs from GitHub's runners and only compares status

codes. It does not check page content, response time or certificate expiry.

  • The keepalive is not yet proven by a full cycle. It was added after the shutdown; whether it

keeps the schedule alive across the next 60-day window will show in the run history.

  • Single purpose. The site URL is written into the workflow, and the repository has no license

file. It is a working setup for one site, not a reusable tool.

Source code: github.com/muhittincamdali/site-uptime

Tags

#github-actions#monitoring#uptime#cron#devops
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

Share