Skip to content
Blog

How to monitor a website with GitHub Actions

A scheduled GitHub Actions workflow that checks your site and fails when it is down, the limits that make it a stopgap, and where an uptime monitor takes over.

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:

  • schedule with */5 * * * * asks for a run every five minutes, the shortest interval GitHub allows.
  • workflow_dispatch adds a "Run workflow" button, so you can test it without waiting.
  • --max-time 20 stops a hanging server from holding the job open. A timeout comes back as code 000, which the check treats as down.
  • exit 1 fails 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.