How to Build a Free Python Uptime Monitor with GitHub Actions

Build a free, lightweight website uptime monitor using Python and GitHub Actions. Get instant outage alerts on Discord, Slack, or Telegram.

We spend hours coding automation scripts, setting up APIs, and deploying bots, but how often do we actually monitor them?

Most of us wait for a user or a log error to tell us that something broke. It's a reactive way to build, and it's a quick way to lose trust.

Most developers solve this problem using monitoring services like Pingdom or Datadog. These tools are honestly very powerful. They can monitor uptime, track performance, and send alerts almost instantly. But for beginners, students, side projects, or small websites, the monthly pricing can feel unnecessary.

Sometimes you just want something simple.

You want a script that quietly checks your website every few minutes and instantly alerts you if something goes wrong.

The good news is that Python makes this surprisingly easy.

Automated website uptime monitor workflow using Python and GitHub Actions.

In this guide, you will learn how to build a lightweight website uptime monitor using Python, Discord webhooks, and GitHub Actions. The setup is completely free and runs automatically in the cloud every few minutes without keeping your laptop on.


Why Build Your Own Website Monitor?

There are already many uptime monitoring tools online, so it is fair to ask why you would even build your own.

The answer mostly comes down to flexibility and cost.

Free plans from popular monitoring tools usually come with restrictions. Some only check your website every 5 minutes. Others lock important notification features behind paid plans.

When you build your own monitor, you control everything.

  • How often the site gets checked
  • Where alerts are sent
  • What counts as a failure
  • How the notifications look
  • Where the script runs

And since this project uses basic Python, you can expand it later however you want.

Monitoring Tool Cost Check Interval Alert Channels
UptimeRobot (Free) $0 Restricted to 5 mins Email, push notifications
Pingdom (Paid) ~$15+/mo 1 min SMS, Email, integrations
Your Python Script $0 Customizable Discord / Slack / Telegram

Note: GitHub Actions workflows can occasionally run a few minutes later than scheduled, so monitoring intervals may not be exact. For most websites this is perfectly fine, but if you need real-time alerts, a dedicated uptime monitoring service is a better option.


How the Monitoring System Works

The setup itself is actually very simple.

The Python script sends a request to your website or API endpoint. If the website responds normally, the script does nothing. But if the request fails, times out, or returns an error code, the script instantly sends a notification through a webhook.

That notification goes directly to Discord.

GitHub Actions then runs this script automatically every 15 minutes in the cloud, completely for free.

So instead of manually checking your website every few hours, or paying for a monitoring service to do it, the system watches everything for you.


What You Need Before Starting

You do not need a powerful server or complicated setup for this project. The requirements are very small.

  • A GitHub account
  • Python 3.8 or newer
  • A Discord server where you want alerts to appear
  • The requests library

Create a new GitHub repository before continuing. Inside your project folder, create these four files:

Short on time?

If you don't want to create and copy-paste each file manually, I have built a fully configured template repository for this project on GitHub. Fork the complete repository on GitHub, then jump straight to setting up your Discord webhook to get it running in less than 2 minutes.

monitor.py
requirements.txt
monitor_state.json
.github/workflows/uptime-monitor.yml

We will fill each one in as we go.


Creating the requirements.txt File

Open requirements.txt and add this single line:

requests

That is the only dependency this project needs. GitHub Actions will install it automatically every time the script runs.


Creating the monitor_state.json File

Now open monitor_state.json and add:

{}

This file is what prevents your monitor from spamming you during a long outage.

Without it, the script would send a fresh alert every single time GitHub Actions runs. If your website stays offline for an hour and the script checks every 15 minutes, you would receive four alerts for the same problem.

Instead, the monitor remembers whether a site was already down during the last check, and only sends a new alert if the status actually changed. When the site comes back online, it sends one recovery message.

Worth knowing: The URLs you monitor may appear in your repository history. If you are monitoring private or internal services, consider using a private repository. GitHub's free tier also includes 2,000 Actions minutes per month for private repositories, although this lightweight project uses very little of that allowance.


The Python Uptime Monitor Script

Now open monitor.py and paste in the full script below.

import requests
import json
import os
from datetime import datetime

# ==============================
# CONFIGURATION
# ==============================

URLS_TO_MONITOR = [
    "https://example.com",
    "https://api.example.com/health"
]

TIMEOUT_SECONDS = 10
MAX_RETRIES = 2          # Number of retries before treating a site as down
RETRY_DELAY_SECONDS = 3  # Seconds to wait between retries

WEBHOOK_URL = os.environ.get("WEBHOOK_URL")

STATE_FILE = "monitor_state.json"

# ==============================
# LOAD STATE
# ==============================

def load_state():
    if not os.path.exists(STATE_FILE):
        return {}
    try:
        with open(STATE_FILE, "r") as file:
            return json.load(file)
    except:
        return {}

# ==============================
# SAVE STATE
# ==============================

def save_state(state):
    with open(STATE_FILE, "w") as file:
        json.dump(state, file)

# ==============================
# SEND ALERT
# ==============================

def send_alert(message):
    payload = {
        "content": message
    }
    try:
        requests.post(WEBHOOK_URL, json=payload, timeout=10)
    except Exception as e:
        print(f"Failed to send alert: {e}")

# ==============================
# CHECK WEBSITE (with retries)
# ==============================

def check_website(url):
    last_error = None

    for attempt in range(1 + MAX_RETRIES):
        try:
            response = requests.get(url, timeout=TIMEOUT_SECONDS)

            if response.ok:
                return True, None

            # Some APIs deliberately return non-2xx codes even when healthy.
            # If that applies to your endpoint, you can adjust this check.
            last_error = f"Website issue detected: {url} returned {response.status_code}"

        except requests.exceptions.Timeout:
            last_error = f"Timeout detected for {url}"

        except requests.exceptions.ConnectionError:
            last_error = f"Connection error for {url}"

        except requests.exceptions.SSLError:
            last_error = f"SSL certificate error for {url} — certificate may be expired or invalid"

        except Exception as e:
            last_error = f"Unexpected error for {url}: {e}"

        if attempt < MAX_RETRIES:
            print(f"Attempt {attempt + 1} failed for {url}. Retrying in {RETRY_DELAY_SECONDS}s...")
            import time
            time.sleep(RETRY_DELAY_SECONDS)

    return False, last_error

# ==============================
# MAIN
# ==============================

def main():
    state = load_state()

    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    print(f"\nRunning uptime check at {current_time}\n")

    for url in URLS_TO_MONITOR:

        is_up, error_message = check_website(url)

        previous_status = state.get(url, "UP")

        # Website went DOWN
        if not is_up and previous_status == "UP":
            send_alert(f"🚨 Website DOWN: {error_message}")
            state[url] = "DOWN"
            print(f"ALERT SENT: {error_message}")

        # Website recovered
        elif is_up and previous_status == "DOWN":
            send_alert(f"✅ Incident Resolved: {url} is back online.")
            state[url] = "UP"
            print(f"{url} recovered")

        # No status change
        else:
            print(f"No change for {url} (currently {previous_status})")

    save_state(state)

if __name__ == "__main__":
    main()

Before we move on, notice this line near the top: WEBHOOK_URL = os.environ.get("WEBHOOK_URL")

Instead of pasting your webhook URL directly into the script, the code reads it from an environment variable. This is important. If you hardcode the URL and accidentally push your code to a public GitHub repository, bots scraping GitHub can steal it within minutes and spam your Discord channel. We will store it safely in just a moment.

Also notice that the script uses response.ok instead of checking only for 200. This is because APIs sometimes return other perfectly valid status codes like 201 Created or 204 No Content. Using response.ok covers the entire range of successful responses automatically.


Setting Up Discord Webhook Alerts

Discord is one of the easiest platforms to use for uptime alerts because it only requires a webhook URL. There are no bot tokens, OAuth apps, or complicated authentication steps.

Here is how to create your Discord webhook:

  1. Open the Discord server where you want alerts to appear
  2. Hover over any text channel and click the gear icon to open "Edit Channel"
    Locating the Discord channel settings gear icon to create a webhook
  3. Click Integrations in the sidebar
  4. Click Create Webhook
    Navigating to the Integrations tab in Discord channel settings
  5. Give the webhook a name like Uptime Monitor
  6. Click Copy Webhook URL
    Creating a new Discord webhook and copying the webhook URL

Your webhook URL will look similar to this: https://discord.com/api/webhooks/123456789012345678/abcdefghijk...

Do not paste this directly into your Python script. In the next step, we will store it securely using GitHub Secrets.


Storing the Webhook URL Safely in GitHub Secrets

GitHub Secrets let you store sensitive values like API keys and webhook URLs privately. They get passed to your script at runtime without ever appearing in your code.

Here is exactly how to add yours:

  1. Open your GitHub repository in the browser
  2. Click Settings in the top navigation bar
  3. In the left sidebar, click Secrets and variables, then click Actions
  4. Click the green New repository secret button
    Adding a new repository secret in GitHub Actions settings for secure environment variables
  5. In the Name field, type exactly: DISCORD_WEBHOOK_URL
  6. In the Secret field, paste the webhook URL you copied from Discord
  7. Click Add secret

That is it. Your webhook URL is now stored securely and will never appear in your code or your commit history.


Creating the GitHub Actions Workflow

Now open .github/workflows/uptime-monitor.yml and paste in the following:

name: Website Uptime Monitor

on:
  schedule:
    - cron: '*/15 * * * *'  # Targets every 15 minutes; actual runs may be slightly delayed
  workflow_dispatch:          # Allows manual runs from the GitHub Actions tab

permissions:
  contents: write

jobs:
  monitor:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Create state file if missing
        run: |
          if [ ! -f monitor_state.json ]; then
            echo '{}' > monitor_state.json
          fi

      - name: Run Monitor Script
        env:
          WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
        run: python monitor.py

      - name: Save updated state
        run: |
          git config user.name "github-actions"
          git config user.email "[email protected]"
          git add monitor_state.json
          git diff --cached --quiet || git commit -m "Update monitor state"
          git push

Let's quickly go through what is happening here.

The schedule line tells GitHub to run the script automatically every 15 minutes. The workflow_dispatch line is what allows you to run it manually from the GitHub Actions tab whenever you want, which is very useful for testing.

The Create state file if missing step makes sure monitor_state.json always exists before the script runs, even if something went wrong with the file previously.

The Save updated state step at the bottom is the most important part. After the script runs and updates which sites are up or down, this step commits those changes back to your repository. This is what actually prevents alert spam, as I explained earlier.

Worth knowing: If your repository uses branch protection rules that require pull request reviews, the git push step may fail. In that case, you can either disable that rule for the github-actions bot, or switch to a different state storage method like GitHub Actions cache (actions/cache).

The env block is where GitHub pulls your secret and makes it available to the script as an environment variable. That is how os.environ.get("WEBHOOK_URL") in your Python file picks it up.


Testing the Monitor Manually

Before waiting 15 minutes for the first scheduled run, you can trigger the workflow yourself to make sure everything is working perfectly.

  1. Make sure your latest changes are saved on GitHub (if you built this locally, run git push to upload your files first).
  2. Go to your GitHub repository in your browser and click the Actions tab at the top.
  3. Click on Website Uptime Monitor in the left sidebar.
  4. On the right side of the page, click the Run workflow dropdown button.
  5. Click the green Run workflow button inside the dropdown.
    Manually triggering a scheduled Python uptime monitor workflow using the GitHub Actions run workflow dropdown

GitHub will immediately start a run. Click on it to watch the live console output. You will see each URL being checked and the result printed line by line.

If everything is set up correctly, your Discord channel will stay quiet because your websites are presumably online. To confirm alerts are actually working, temporarily change one of your URLs in monitor.py to https://httpstat.us/500, push the change, and run the workflow manually again. You should receive a Discord alert within seconds. Then revert the URL back.

The first time you see a Discord alert come through from your own monitoring system, it feels genuinely satisfying.


Using Slack Instead of Discord

If you use Slack for work or team communication, switching is straightforward. You only need to change one line of code and update two places in your workflow file.

Step 1: Get a Slack Webhook URL

  1. Go to api.slack.com/apps and click Create an App
  2. Choose From scratch, give it a name, and select your workspace
  3. Click Create App
  4. Click Incoming Webhooks in the left sidebar and toggle it on
  5. Click Add New Webhook to Workspace and choose a channel
  6. Click Allow on Review app permissions
  7. Copy the webhook URL it generates

Step 2: Add the Secret to GitHub

Go to your GitHub repository secrets configuration and create a new repository secret.

Name it SLACK_WEBHOOK_URL and paste your Slack webhook URL as the value.

Step 3: Update the Workflow File

Open .github/workflows/uptime-monitor.yml. Find this line:

WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

Replace it with:

WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Step 4: Update the Python Script

Open monitor.py. Find the send_alert function and look for this line inside it:

"content": message

Replace it with:

"text": message

That is the only code change needed. Slack expects the message inside a field called text instead of content. Everything else stays exactly the same.


Using Telegram Instead

Telegram works a bit differently because it uses a bot API rather than a simple webhook URL. It takes a couple more steps to set up, but alerts arrive directly on your phone.

Step 1: Create a Telegram Bot

  1. Open Telegram and search for @BotFather
  2. Click "Start Bot"
  3. Send the message /newbot
  4. Follow the prompts to choose a name and username for your bot
  5. BotFather will give you a bot token that looks like: 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ copy it

Warning! you need to find your bot by its username, and send it any message such as /start. This is required before moving on. Telegram bots cannot message you first, so if you skip this step your alerts will fail with a "chat not found" error even if everything else is configured correctly.

Step 2: Get Your Chat ID

  1. Search for @userinfobot on Telegram and start a conversation
  2. It will reply with your account details including your Chat ID copy that number

Step 3: Add Both Secrets to GitHub

Navigate to your GitHub repository secrets page and add two separate repository secrets:

  • Secret 1: Name it TELEGRAM_BOT_TOKEN and paste your token from @BotFather.
  • Secret 2: Name it TELEGRAM_CHAT_ID and paste your numerical ID from @userinfobot.

Step 4: Update the Workflow File

Open .github/workflows/uptime-monitor.yml. Find the env block and replace it entirely with:

        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}

Step 5: Update the Python Script

Open monitor.py. Replace the entire send_alert function with this version:

def send_alert(message):
    bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
    chat_id = os.environ.get("TELEGRAM_CHAT_ID")
    telegram_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    payload = {
        "chat_id": chat_id,
        "text": message
    }
    try:
        requests.post(telegram_url, json=payload, timeout=10)
    except Exception as e:
        print(f"Failed to send alert: {e}")

Alerts will now arrive directly in your Telegram chat. Everything else in the script stays exactly the same.


Real-World Uses for This Monitor

This type of Python uptime monitoring system can be used for much more than websites.

  • Monitoring personal blogs
  • Checking automation APIs
  • Tracking Discord bot health endpoints
  • Watching VPS landing pages
  • Monitoring portfolio sites
  • Checking internal tools for clients

Once you understand this workflow, you can automate monitoring for almost anything online.


Final Thoughts

You do not always need expensive enterprise software to build reliable automation systems.

Sometimes a small Python script plus GitHub Actions is more than enough.

In this project, you built a free Python website uptime monitor that runs automatically in the cloud, sends instant alerts when something fails, prevents alert spam during long outages, and securely stores credentials using GitHub Secrets.

What platform are you planning to send alerts to? Drop a comment below.

Take it to the next level: Now that you have a reliable monitoring system in place, put it to the test by building an automated project! Check out my step-by-step guide on how to build a free Bluesky automation bot using Gemini AI and GitHub Actions.

Copyright (c):
procwire.com

About the author

Stephano kambeta
Proudly African 🇲🇼. Modern life, shaped by the echoes of traditional drums.

Post a Comment