Cloudflare Worker uptime monitoring with a D1-backed status store, a Durable Object scheduler, and durable notification delivery.
It serves a static status page, runs checks every minute, records monitoring state in D1, and keeps notification work in a D1 outbox until delivery is confirmed.
flowchart LR
Cron["Cron Trigger\nevery minute"] --> Scheduler["Scheduler Durable Object\nsingle active run"]
Scheduler --> Checks["HTTP / TCP checks"]
Checks --> Local["Worker colo"]
Checks --> Remote["RemoteChecker Durable Object\nlocation hint"]
Checks --> Globalping["Globalping measurement"]
Scheduler --> D1[("D1\nstate, runs, outbox")]
Scheduler --> Delivery["Webhook dispatcher"]
Delivery --> Webhook["Notification endpoint"]
Scheduler --> Heartbeat["HEARTBEAT_URL\nexternal dead-man's switch"]
Browser["Status-page visitor"] --> API["/api/data, /api/health"]
API --> D1
- A static status page with
/api/data, badges, and/api/health. - A singleton Scheduler Durable Object prevents overlapping scheduled runs.
- D1 stores state, run records, and a durable notification outbox; deliveries are at-least-once.
- Local Worker, Durable Object location-hint, Globalping, and allowlisted custom-proxy checks.
- An optional external heartbeat reports that the monitor itself completed a run.
- Fork this repository, clone your fork, then run
npm ci(Node.js 22.13.0 or later). - Edit
uptime.config.tsto add your monitors and status-page metadata. - Create the D1 database with
npm run d1:create, then copy the returned database ID into theUPTIME_WORKER_D1binding inwrangler.jsonc. Initialize it withnpm run d1:migrate:remote. - Validate the artifact with
npm run deploy:dry-run, then runnpm run deployonce to create the Worker. - Set every secret used by your enabled configuration (the table below lists the names) and verify
/api/health.npx wrangler secret put <SECRET_NAME>updates an existing Worker, so it follows the initial deploy.
For local development, create a gitignored .dev.vars file beside wrangler.jsonc and use only placeholders, for example TG_BOT_TOKEN=<SECRET_VALUE>. Then run npm run d1:migrate:local followed by npm run dev. Do not commit .dev.vars.
To deploy a fork, make the same D1 binding and secrets in the fork's Cloudflare account. The Worker name, D1 database name, Durable Object bindings, migrations, static-assets binding, and cron trigger are defined in wrangler.jsonc; keep documentation and automation aligned with those names.
After the Worker exists, set a deployed secret interactively with npx wrangler secret put <SECRET_NAME>; replace <SECRET_NAME> with a name from this table. The command's prompt receives the value, so values never need to appear in shell history or source control. Cloudflare documents that wrangler secret put creates and immediately deploys a new Worker version; use the version-based workflow when you need gradual deployment. Workers secrets
| Secret | Used by | Required when |
|---|---|---|
TG_BOT_TOKEN |
Telegram webhook URL in the sample notification | Telegram notification is enabled |
TG_CHAT_ID |
Telegram webhook payload in the sample notification | Telegram notification is enabled |
VPS1_IP |
TCP monitor target in the sample configuration | That monitor is enabled |
VPS1_PORT |
TCP monitor target; defaults to 22 at runtime |
A non-default port is needed |
HOMELAB_HOST |
TCP monitor target in the sample configuration | That monitor is enabled |
HOMELAB_PORT |
TCP monitor target in the sample configuration | That monitor is enabled |
HEARTBEAT_URL |
External dead-man's-switch GET request | Monitoring the monitor is enabled |
Every <SECRET_NAME> reference in uptime.config.ts is resolved before a scheduled run begins. A missing, empty, or unresolved value makes that run fail closed: no fabricated target or notification is used. HEARTBEAT_URL is deliberately optional; if absent, the external heartbeat is skipped rather than treated as a check target.
Deployments and local development apply the ordered SQL files in migrations/. Do not run deploy/init.sql during normal deployment.
- Create the database with
npm run d1:create. - Put the returned database ID in
wrangler.jsonc. - Initialize the remote database with
npm run d1:migrate:remote. - For local development, run
npm run d1:migrate:localto apply the same migrations locally.
For an existing installation that was initialized from deploy/init.sql, keep the existing database and its ID. Run wrangler d1 migrations apply uptime_worker_d1 --remote once before the next deployment. The migrations use idempotent CREATE ... IF NOT EXISTS statements, so this records the migration history without replacing existing tables or data.
deploy/init.sql remains only as a compatibility schema snapshot for legacy/manual recovery. CI and package scripts use D1 migrations, and future schema changes belong in migrations/.
Set HEARTBEAT_URL to the unique HTTPS ping URL supplied by an external service such as Healthchecks or Better Stack. After a persisted scheduled run, the Scheduler sends one GET request to that URL. Configure the external monitor for a one-minute expected period and a three-minute grace period: this mirrors the Worker cron and /api/health becoming stale after 180 seconds. A successful heartbeat means the monitoring run reached that boundary; it does not mean every target was up.
Use /api/health for a second, direct health signal. It returns HTTP 200 only when the most recent state is healthy; it returns 503 while initializing, delayed, stale, or unavailable. See operations for expected responses and incident procedures.
Without checkProxy, a check runs from the current Worker colo and a public three-letter colo value may be displayed. worker://LOCATION_HINT routes the check through a RemoteChecker Durable Object named from the monitor ID and hint; the hint influences Durable Object placement but does not promise a specific city, and the location shown is the colo observed by that Worker invocation.
globalping://TOKEN?magic=LOCATION submits a Globalping measurement. Its public location is the result's country/city, not a Cloudflare colo. Custom HTTP(S) proxy locations are intentionally never exposed in the public API. A custom proxy must set checkProxyAllowedHosts to its hostname; see uptime.config.ts for the safe example.
Secrets stay in Cloudflare secret storage or .dev.vars, never in wrangler.jsonc, source, issues, or logs. The custom proxy receives a small monitor DTO over POST; it does not receive the monitored request's Authorization or Cookie headers, and redirects are rejected.
Logs use an allowlist of operational fields only: event name, monitorId, runId, status/boolean values, duration, ping, location/measurement identifiers, webhook host and method, and error category. They must not include webhook URLs, tokens, request headers, bodies, target URLs when sensitive, or external heartbeat URLs.
Cloudflare limits and pricing vary by plan and change over time. Before sizing monitors, read the official Workers limits, D1 limits, and pricing pages rather than relying on a copied free-tier quota.