Python uptime monitor

An uptime monitor checks one or more URLs, records status and latency, and alerts only when a site changes between up and down. Tokay runs the Python script as a Scheduled Service and preserves its SQLite history.

Last updated

Start with Google and Example Domain to see two healthy checks before pointing it at your own service. The script exits after each pass, which keeps its schedule and failures easy to inspect.

Runtime
Python
Framework
None
Service types
Scheduled Service
Resources
SQLite
Required secrets
None

What it does

Each run prints a result for every configured URL with its HTTP status, response time, and current up or down state.

The first observation establishes a baseline. A later state change writes one ALERT line and can send the same message to Slack or Discord.

Unchanged outages stay quiet on later runs, while the SQLite row count continues to show that checks are still happening.

Deploy it

Bring this folder to Tokay any way you like. Paste or upload it in the dashboard, push it with git, or hand it to your AI agent along with our agent instructions. Tokay figures out the rest.

Working in Claude, ChatGPT, VS Code, or another MCP client? Connect the Tokay MCP server and ask your agent to deploy this example. It signs in through your browser, so there is no token to paste.

New to Tokay? The getting started guide walks you through your first deploy.

Try it

  1. Choose Run Now twice and compare the accumulated row count in both results.
  2. Open Saved Files and confirm that uptime.sqlite3 is present.
  3. Deploy again and run one more check. The count continues from the same database.

Watch your own site

Open the Service's Environment page and find MONITOR_URLS under From your code. Choose Set shared value and enter one URL or a comma separated list. If you leave off the scheme, the monitor uses https.

The first check records the starting state. A later change writes an ALERT line, so healthy runs and an unchanged outage do not keep notifying you.

To send that alert to Slack or Discord, add an incoming webhook as ALERT_WEBHOOK_URL. The run log still records the alert if delivery fails.

How it works

You get one alert per change because the script reads the last saved state before inserting the new result. It delivers a webhook only when those states differ, then exits normally.

Tokay starts the next pass on schedule and keeps its run output. The SQLite file survives deployments and restarts. Read Files and persistent storage for the storage details.

Secrets

Name Required Purpose
ALERT_WEBHOOK_URL No Sends state changes to a Slack or Discord incoming webhook.

Grow from here

Try the Playwright price watcher when each check needs a real browser and screenshot.

monitor.py Raw

import json
import os
import sqlite3
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def normalized_urls():
    configured = os.getenv("MONITOR_URLS", "https://www.google.com,https://example.com")
    urls = []
    for value in configured.split(","):
        value = value.strip()
        if not value:
            continue
        urls.append(value if "://" in value else f"https://{value}")
    if not urls:
        raise ValueError("MONITOR_URLS did not contain any URLs")
    return urls


def check(url):
    started = time.perf_counter()
    request = Request(url, headers={"User-Agent": "Tokay uptime monitor example"})
    try:
        with urlopen(request, timeout=12) as response:
            status_code = response.status
            response.read(64)
        status = "up" if 200 <= status_code < 400 else "down"
        error = None
    except HTTPError as problem:
        status_code = problem.code
        status = "down"
        error = str(problem)
    except (URLError, TimeoutError, OSError) as problem:
        status_code = None
        status = "down"
        error = str(problem)
    latency_ms = round((time.perf_counter() - started) * 1000)
    return status, status_code, latency_ms, error


def deliver_alert(message):
    webhook = os.getenv("ALERT_WEBHOOK_URL", "").strip()
    if not webhook:
        return "log"
    payload = json.dumps({"content": message, "text": message}).encode()
    request = Request(webhook, data=payload, headers={"Content-Type": "application/json"}, method="POST")
    try:
        with urlopen(request, timeout=10) as response:
            response.read(64)
        return "webhook"
    except Exception as problem:
        print(f"Alert delivery failed. {problem}")
        return "log"


def main():
    data_dir = Path(os.getenv("TOKAY_DATA_DIR", "./data"))
    data_dir.mkdir(parents=True, exist_ok=True)
    database = data_dir / "uptime.sqlite3"
    connection = sqlite3.connect(database)
    connection.execute(
        """
        CREATE TABLE IF NOT EXISTS checks (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          checked_at TEXT NOT NULL,
          url TEXT NOT NULL,
          status TEXT NOT NULL,
          status_code INTEGER,
          latency_ms INTEGER NOT NULL,
          error TEXT
        )
        """
    )
    checked_at = datetime.now(timezone.utc).isoformat()
    results = []
    alerts = 0
    for url in normalized_urls():
        previous = connection.execute(
            "SELECT status FROM checks WHERE url = ? ORDER BY id DESC LIMIT 1", (url,)
        ).fetchone()
        status, status_code, latency_ms, error = check(url)
        connection.execute(
            "INSERT INTO checks (checked_at, url, status, status_code, latency_ms, error) VALUES (?, ?, ?, ?, ?, ?)",
            (checked_at, url, status, status_code, latency_ms, error),
        )
        if previous and previous[0] != status:
            message = f"ALERT {url} changed from {previous[0]} to {status} after {latency_ms} ms"
            delivery = deliver_alert(message)
            print(f"{message} via {delivery}")
            alerts += 1
        results.append({"url": url, "status": status, "status_code": status_code, "latency_ms": latency_ms})
    connection.commit()
    total_checks = connection.execute("SELECT count(*) FROM checks").fetchone()[0]
    connection.close()
    print(json.dumps({"event": "uptime_pass", "checked_at": checked_at, "alerts": alerts, "total_checks": total_checks, "results": results}))


if __name__ == "__main__":
    main()

Built from revision 52c7b7f

Guide: Run a Python script every day