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
- Choose Run Now twice and compare the accumulated row count in both results.
- Open Saved Files and confirm that
uptime.sqlite3is present. - 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.
Source code
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