You can monitor a website with GitHub Actions in about fifteen lines of YAML: a workflow on a schedule, one curl, and a failing step when the site does not answer. It costs nothing on a public repository, it lives next to your code, and GitHub emails you when a run fails. For a side project that is often enough.
It is also a monitor with three blind spots that GitHub documents itself: it cannot check more often than every five minutes, scheduled runs can be late or skipped, and it only ever looks from wherever GitHub's runner happens to be. This post gives you the workflow, then the limits, then the point where a real uptime monitor is worth switching to.
The workflow
Save this as .github/workflows/uptime.yml:
name: Uptime check
on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Check the site answers
run: |
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 20 https://example.com/)
echo "HTTP $code"
if [ "$code" -lt 200 ] || [ "$code" -ge 400 ]; then
echo "::error::https://example.com/ returned $code"
exit 1
fi
What each part does:
schedulewith*/5 * * * *asks for a run every five minutes, the shortest interval GitHub allows.workflow_dispatchadds a "Run workflow" button, so you can test it without waiting.--max-time 20stops a hanging server from holding the job open. A timeout comes back as code000, which the check treats as down.exit 1fails the run, and a failed scheduled run sends an email to whoever last edited the schedule.
To check more than one page, repeat the step or loop over a list of URLs. To check that the page says what it should, not just that it answered, fetch the body and grep for a string you expect, so a blank 200 or an error page served with a 200 still fails.
Getting the alert somewhere useful
GitHub's own failure email goes to one person and is easy to filter away. Add a final step that only runs when the check fails and posts to wherever your team actually looks:
- name: Tell the team
if: failure()
run: |
curl -s -X POST -H "Content-Type: application/json" \
-d '{"text":"example.com is down (GitHub Actions uptime check)"}' \
"${{ secrets.SLACK_WEBHOOK_URL }}"
Store the webhook as a repository secret, never in the file.
The limits, from GitHub's own documentation
These are not edge cases. They are how scheduled workflows are specified to behave, per GitHub's documentation on the schedule event.
| Limit | What it means for monitoring |
|---|---|
| Five minutes is the shortest schedule | An outage can run for up to five minutes before the next check even starts. |
| Runs can be delayed under high load, and the start of every hour is a known busy time | Your "every five minutes" is a request, not a promise. |
| Queued scheduled runs can be dropped when load is high | Some checks simply never happen, and nothing tells you. |
| On a public repository, schedules are disabled after 60 days with no repository activity | A quiet repo stops monitoring itself, silently, two months in. |
| You do not choose where the runner is | Every check comes from one place. A site that is down in Europe but up from the runner's network passes. |
| No alert on recovery, no history, no status page | You learn it went down. You do not learn when it came back, how often it happens, or tell your users. |
There is also cost on a private repository. GitHub bills each job rounded up to a whole minute, so a five-minute schedule is about 8,640 billed minutes a month. That is more than four times the 2,000 included minutes on GitHub's Free plan for private repositories (GitHub's billing documentation). Public repositories on standard runners are free.
When the workflow is enough
Keep the GitHub Actions check if all of these are true:
- It is a personal project or an internal tool, and nobody outside the team depends on it.
- Finding out several minutes late is fine.
- The repository is public, or you have minutes to spare.
- You would not be embarrassed if a regional outage went unnoticed.
When to use an uptime monitor instead
Switch once users or customers depend on the site. A dedicated monitor checks on its own schedule, not as a best-effort job, and checks from more than one place, so a regional failure shows up as regional instead of being missed or mistaken for a full outage.
RealUptime's free plan covers what the workflow cannot: 10 monitors checked every 3 minutes from four regions (US-East, US-West, Europe and Asia-Pacific), alerts by email, Slack, PagerDuty and webhooks when a check fails and when it recovers, a history of every reading, and a public status page. It takes a URL and about a minute to set up, with no card.
Keep GitHub Actions for what it is good at: deploy gates
Workflows are the wrong tool for watching a site around the clock, but the right tool for asking "is production healthy?" at a specific moment, such as right after a deploy. RealUptime publishes a GitHub Action for exactly that. It reads your monitors' current status and fails the step if any of them are down or degraded:
- name: Confirm production is healthy after deploy
uses: RealUptimeHQ/check-status-action@v1
with:
api-key: ${{ secrets.REALUPTIME_API_KEY }}
check-ids: 11111111-1111-4111-8111-111111111111
The monitor does the watching, every minute and from every region you pick. The workflow asks it one question at the moment it matters. That split gives you continuous monitoring without spending Actions minutes on it, and a deploy pipeline that stops when the site it just shipped is not answering.