Cloudsteading
Uptime Guard service detail: current status, 24-hour to 90-day uptime and the recent-check bar
Uptime Guard service detail: current status, 24-hour to 90-day uptime and the recent-check bar

Uptime Guard

Uptime checks, escalating alerts and public status pages from one Worker

Uptime Guard is a self-hosted Better Stack/Pingdom alternative built on Cloudflare (D1, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@CalmRay-Solutions

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

A per-minute cron is about 1,440 Worker invocations a day, inside the Workers Free daily request allowance; D1 history is rolled up and pruned daily. Very large fleets can hit Free plan limits on CPU time and subrequests per invocation.

Hosting requirements
  • Checks run from Cloudflare’s network on a one-minute cron, not from chosen probe regions.
  • Telegram and Web Push alerts are optional and need their own bot or VAPID keys.
  • The root and worker/ Wrangler files describe the same Worker; the root one exists for the Deploy to Cloudflare button.
  • Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
  • Official Cloudflare pricing pages saved 31 Oct 2026 (Markdown editions of developers.cloudflare.com pricing documentation).
Check current pricing ↗
Review findings & limitations
  • Checks run from Cloudflare’s network on a one-minute cron, not from chosen probe regions.
  • Telegram and Web Push alerts are optional and need their own bot or VAPID keys.
  • The root and worker/ Wrangler files describe the same Worker; the root one exists for the Deploy to Cloudflare button.
  • Competitor links describe the stated workflow overlap, not feature parity or a verified price saving.
  • Source review at the pinned commit; deployment, workload CPU and application security have not been independently tested.
Sources checked 31/10/2026

Repository snapshot: 93b2c90. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • uptimerobot ↗

    Every other uptime tool makes you choose: **pay per monitor** (UptimeRobot, Pingdom, Better Uptime)

  • better-stack ↗

    Every other uptime tool makes you choose: **pay per monitor** (UptimeRobot, Pingdom, Better Uptime)

  • pingdom ↗

    Every other uptime tool makes you choose: **pay per monitor** (UptimeRobot, Pingdom, Better Uptime)

  • workers ↗

    button # and with `npx wrangler deploy`. The [build] step builds the dashboard first, so # a single command deploys everything. name = "uptime-guard" main = "src/index.ts" compatibility_date = "2024-11-01" # Bundle schema.sql as text so the Worker can create tables on first run. rules = [{ type = "Text", globs = ["**/*.sql"], fallthrough = true }] # Build the React dashboard before deploying (its output is served by the Worker). [build] command = "cd ../dashboard && npm install && npm run build"

  • d1 ↗

    "0 3 * * *"] [assets] directory = "../dashboard/dist" binding = "ASSETS" not_found_handling = "single-page-application" [[d1_databases]] binding = "DB" database_name = "uptime-guard" # The Deploy-to-Cloudflare button provisions this automatically. For a manual # deploy, run `npx wrangler d1 create uptime-guard` and paste the id here. database_id = "PLACEHOLDER_RUN_D1_CREATE" # Optional: serve on your own domain (must be on this Cloudflare account). # routes = [{ pattern = "status.example.com",

  • free-tier-eligible ↗

    - **Workers**: 100k requests/day free - a per-minute cron is ~1,440/day.

  • free-tier-eligible ↗

    | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |

  • free-tier-eligible ↗

    | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |

  • MIT ↗

    MIT License Copyright (c) 2026 CalmRay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION

  • architecture ↗

    crons = ["* * * * *", "0 3 * * *"]

  • architecture ↗

    It runs entirely on **Cloudflare Workers + D1**

  • architecture ↗

    button # and with `npx wrangler deploy`. The [build] step builds the dashboard first, so # a single command deploys everything. name = "uptime-guard" main = "src/index.ts" compatibility_date = "2024-11-01" # Bundle schema.sql as text so the Worker can create tables on first run. rules = [{ type = "Text", globs = ["**/*.sql"], fallthrough = true }] # Build the React dashboard before deploying (its output is served by the Worker). [build] command = "cd ../dashboard && npm install && npm run build"

  • architecture ↗

    "0 3 * * *"] [assets] directory = "../dashboard/dist" binding = "ASSETS" not_found_handling = "single-page-application" [[d1_databases]] binding = "DB" database_name = "uptime-guard" # The Deploy-to-Cloudflare button provisions this automatically. For a manual # deploy, run `npx wrangler d1 create uptime-guard` and paste the id here. database_id = "PLACEHOLDER_RUN_D1_CREATE" # Optional: serve on your own domain (must be on this Cloudflare account). # routes = [{ pattern = "status.example.com",

Upstream screenshot · CalmRay-Solutions/uptime-guard repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

UptimeRobot logoUptimeRobot ↗

Scheduled availability checks, failure alerts, uptime history and public status pages; multi-region probing, SMS/phone alerts and on-call scheduling are not included

See supporting source ↗
Better Stack logoBetter Stack ↗

Scheduled availability checks, failure alerts, uptime history and public status pages; multi-region probing, SMS/phone alerts and on-call scheduling are not included

See supporting source ↗
Pingdom logoPingdom ↗

Scheduled availability checks, failure alerts, uptime history and public status pages; multi-region probing, SMS/phone alerts and on-call scheduling are not included

See supporting source ↗
Workflow overlap
Pricing not verified
→ D1 + Workers
Workflow overlap
Pricing not verified
→ D1 + Workers

How it works

The shape of Uptime Guard on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points2
uptime-guard
worker/wrangler.toml
uptime-guard
wrangler.toml
↓
App
uptime-guard
entry
Cloudflare Workers
Entrypoint: src/index.tsConfigured cron (UTC): * * * * *Configured cron (UTC): 0 3 * * *
uptime-guard
entry
Cloudflare Workers
Entrypoint: worker/src/index.tsConfigured cron (UTC): * * * * *Configured cron (UTC): 0 3 * * *
↓

Configuration and workflow sources

Reviewed commit 93b2c904721b. This configuration evidence comes from reading source files. Execution checks, when available, appear in the project's runtime review.

Deployment configuration · 2 files
worker/wrangler.toml ↗

Cloudflare Workers · compatibility 2024-11-01

uptime-guard · default

Entrypoint: src/index.ts

Build: cd ../dashboard && npm install && npm run build

Static assets: ../dashboard/dist · single-page-application

Cron triggers (UTC): * * * * * · 0 3 * * *

  • DB → D1
  • ASSETS → Static assets
wrangler.toml ↗

Cloudflare Workers · compatibility 2024-11-01

uptime-guard · default

Entrypoint: worker/src/index.ts

Build: npm --prefix dashboard ci && npm --prefix dashboard run build

Static assets: dashboard/dist · single-page-application

Cron triggers (UTC): * * * * * · 0 3 * * *

  • DB → D1
  • ASSETS → Static assets

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

worker/src/index.ts ↗
  • L857 · fetch handler exported · references DB, PASSWORD, TOTP_SECRET, DEMO_MODE, ASSETS · calls filter, url.pathname.split, ensureSchema, req.headers.get, Date.now, first, bind, env.DB.prepare, json, req.json, getSetting, Boolean, verifyPassword, timingSafeEqual, verifyTotp, run, Math.min, setTimeout, createSessionToken, getSessionSecret, currentSessionEpoch, String, recordCheck, setupRequired, generateTotpSecret, encodeURIComponent, setSetting, hashPassword, publicStatus, env.ASSETS.fetch, requireAuth, handlePushRoute, handleSettingsRoute, all, uid, body.name.trim, ctx.waitUntil, caches.default.delete, slice, replace, crypto.randomUUID, Promise.all, map, reverse, Math.round, prepareService, updateService, Number, url.searchParams.get, getServiceStats, checkService
  • L1213 · scheduled handler exported · references DEMO_MODE · calls ensureSchema, ctx.waitUntil, rollupDaily, pruneOldData, runDueChecks
  • L32 · json calls (conditional paths may differ): JSON.stringify
  • L39 · uid calls (conditional paths may differ): crypto.randomUUID
  • L88 · buildHttp calls (conditional paths may differ): body.keyword.trim, body.json_path.trim, String, JSON.stringify
  • L110 · buildTcp calls (conditional paths may differ): Number, JSON.stringify
  • L118 · buildDns calls (conditional paths may differ): toUpperCase, body.expected.trim, JSON.stringify
  • L127 · buildHeartbeat calls (conditional paths may differ): replace, crypto.randomUUID, JSON.stringify
  • L133 · buildTls calls (conditional paths may differ): Number, JSON.stringify
  • L143 · buildDomain calls (conditional paths may differ): Number, JSON.stringify
  • L156 · buildScript calls (conditional paths may differ): parseScript, String, Math.min, Math.max, slice, JSON.stringify
  • L186 · prepareService calls (conditional paths may differ): toLowerCase, builder
  • L208 · updateService calls (conditional paths may differ): first, bind, env.DB.prepare, json, prepareService, String, run
  • L241 · handlePushRoute calls (conditional paths may differ): json, req.json, run, bind, env.DB.prepare, Date.now
  • L265 · notifyPush calls (conditional paths may differ): all, env.DB.prepare, sendPush, run, bind
  • L280 · telegramConfig calls (conditional paths may differ): all, env.DB.prepare, map, m.get
  • L292 · sendTelegram calls (conditional paths may differ): telegramConfig, catch, postTelegram
  • L298 · postTelegram calls (conditional paths may differ): Number, fetch, JSON.stringify
  • L321 · reminderGapMs calls (conditional paths may differ): Math.min
  • L324 · fmtDowntime calls (conditional paths may differ): Math.max, Math.round, Math.floor
  • L332 · recordCheck calls (conditional paths may differ): Date.now, run, bind, env.DB.prepare, openIncident, remindIfDue, resolveIncident
  • L374 · alertHeadline calls (conditional paths may differ): fmtDowntime, Math.floor, Date.now, slice, toUTCString
  • L391 · openIncident calls (conditional paths may differ): run, bind, env.DB.prepare, alertHeadline, sendTelegram, faultLine, notifyPush
  • L410 · remindIfDue calls (conditional paths may differ): first, bind, env.DB.prepare, reminderGapMs, run, alertHeadline, sendTelegram, faultLine, fmtDowntime
  • L437 · resolveIncident calls (conditional paths may differ): first, bind, env.DB.prepare, run, alertHeadline, sendTelegram, notifyPush, fmtDowntime
  • L464 · checkService calls (conditional paths may differ): performCheckConfirmed, recordCheck
  • L474 · currentSessionEpoch calls (conditional paths may differ): Date.now, first, env.DB.prepare, Number
  • L486 · ensureSchema calls (conditional paths may differ): catch, filter, map, split, join, schemaSql.split, startsWith, l.trim, s.trim, env.DB.batch, stmts.map, env.DB.prepare, seedDefaultProject
  • L511 · seedDefaultProject calls (conditional paths may differ): getSetting, first, env.DB.prepare, run, bind, uid, Date.now, setSetting
  • L527 · pbkdf2 calls (conditional paths may differ): crypto.subtle.importKey, encode, crypto.subtle.deriveBits, b64
  • L533 · hashPassword calls (conditional paths may differ): crypto.getRandomValues, b64, pbkdf2
  • L537 · verifyPassword calls (conditional paths may differ): stored.split, timingSafeEqual, pbkdf2, fromB64
  • L545 · getSessionSecret calls (conditional paths may differ): getSetting, replace, crypto.randomUUID, setSetting
  • L558 · setupRequired calls (conditional paths may differ): getSetting
  • L564 · getSetting calls (conditional paths may differ): first, bind, env.DB.prepare
  • L569 · setSetting calls (conditional paths may differ): run, bind, env.DB.prepare
  • L582 · getRetentionDays calls (conditional paths may differ): first, env.DB.prepare, Number, Number.isFinite
  • L591 · rollupDaily calls (conditional paths may differ): Math.floor, Date.now, run, bind, env.DB.prepare
  • L607 · publicStatusOf calls (conditional paths may differ): Math.floor, Date.now, JSON.parse, Number
  • L630 · publicStatus calls (conditional paths may differ): cache.match, first, bind, env.DB.prepare, json, Date.now, Math.floor, all, map, services.map, hist.get, today.get, publicStatusOf, items.filter, resp.headers.set, ctx.waitUntil, cache.put, resp.clone

Environment references: env.DB · env.VAPID_PUBLIC · env.VAPID_PRIVATE · env.VAPID_SUBJECT · env.TELEGRAM_BOT_TOKEN · env.TELEGRAM_CHAT_ID · env.SESSION_SECRET · env.PASSWORD · env.TOTP_SECRET · env.DEMO_MODE · env.ASSETS

worker/src/totp.ts ↗
  • L4 · base32Encode calls (conditional paths may differ): padStart, b.toString, parseInt, bits.slice, padEnd
  • L15 · generateTotpSecret calls (conditional paths may differ): crypto.getRandomValues, base32Encode
  • L21 · base32Decode calls (conditional paths may differ): replace, input.toUpperCase, BASE32_ALPHABET.indexOf, padStart, val.toString, bytes.push, parseInt, bits.slice
  • L36 · hmacSha1 calls (conditional paths may differ): crypto.subtle.importKey, crypto.subtle.sign
  • L48 · totpAt calls (conditional paths may differ): base32Decode, Math.floor, hmacSha1, padStart, String
  • L67 · verifyTotp calls (conditional paths may differ): code.replace, test, Math.floor, Date.now, totpAt, timingSafeEqual
  • L78 · timingSafeEqual calls (conditional paths may differ): a.charCodeAt, b.charCodeAt
worker/src/session.ts ↗
  • L3 · b64url calls (conditional paths may differ): String.fromCharCode, replace, btoa
  • L9 · b64urlToBytes calls (conditional paths may differ): replace, s.replace, slice, atob, str.charCodeAt
  • L17 · hmacSha256 calls (conditional paths may differ): crypto.subtle.importKey, enc.encode, crypto.subtle.sign
  • L30 · createSessionToken calls (conditional paths may differ): JSON.stringify, Date.now, b64url, encode, hmacSha256
  • L38 · verifySessionToken calls (conditional paths may differ): token.split, b64url, hmacSha256, JSON.parse, decode, b64urlToBytes, Date.now
worker/src/checks.ts ↗
  • L71 · expiryResult calls (conditional paths may differ): Number.isFinite, Math.floor, Date.now
  • L90 · parseConfig calls (conditional paths may differ): JSON.parse
  • L99 · readJsonPath calls (conditional paths may differ): reduce, path.split
  • L105 · checkHttp calls (conditional paths may differ): parseConfig, Date.now, setTimeout, controller.abort, fetch, Boolean, res.text, clearTimeout, body.includes, readJsonPath, JSON.parse, String
  • L171 · checkTcp calls (conditional paths may differ): parseConfig, Date.now, setTimeout, reject, connect, Promise.race, catch, socket.close, String
  • L198 · checkDns calls (conditional paths may differ): parseConfig, toUpperCase, Date.now, setTimeout, controller.abort, fetch, encodeURIComponent, clearTimeout, res.json, answers.some, a.data.replace, replace, join, answers.map, String
  • L248 · isCloudflareFronted calls (conditional paths may differ): setTimeout, controller.abort, fetch, clearTimeout, res.headers.has, includes, toLowerCase, res.headers.get
  • L265 · checkTls calls (conditional paths may differ): parseConfig, getCertExpiry, expiryResult, isCloudflareFronted, String
  • L298 · checkDomain calls (conditional paths may differ): parseConfig, Date.now, setTimeout, controller.abort, fetch, encodeURIComponent, clearTimeout, res.json, Date.parse, expiryResult, Number.isNaN, String
  • L343 · checkScript calls (conditional paths may differ): parseConfig, runScript
  • L361 · evaluateHeartbeat calls (conditional paths may differ): Date.now, parseConfig, Math.round
  • L380 · performCheck calls (conditional paths may differ): checkTcp, checkDns, checkTls, checkDomain, checkScript, evaluateHeartbeat, checkHttp
  • L413 · performCheckConfirmed calls (conditional paths may differ): performCheck, sleep
worker/src/script.ts ↗
  • L63 · unquote calls (conditional paths may differ): v.trim, t.endsWith, t.slice
  • L71 · parseExpect calls (conditional paths may differ): exec, at, toLowerCase, trim, step.asserts.push, unquote
  • L123 · parseCapture calls (conditional paths may differ): exec, at, toLowerCase, trim, step.captures.push
  • L141 · parseScript calls (conditional paths may differ): src.split, trim, line.startsWith, exec, line.slice, METHODS.has, word.toUpperCase, at, steps.push, word.toLowerCase, cur.headers.push, parseExpect, parseCapture
  • L194 · readPath calls (conditional paths may differ): reduce, filter, split, path.replace
  • L203 · interpolate calls (conditional paths may differ): text.replace, vars.get
  • L211 · describe calls (conditional paths may differ): slice, JSON.stringify, String
  • L218 · checkAssert calls (conditional paths may differ): String, body.includes, test, json, readPath, Array.isArray, value.map, hay.some, h.includes, describe, Number, Number.isFinite
  • L275 · stepLabel calls (conditional paths may differ): url.includes
  • L294 · runScript calls (conditional paths may differ): Date.now, parseScript, String, Math.min, Math.max, slice, stepLabel, interpolate, fail, setTimeout, controller.abort, fetch, clearTimeout, step.asserts.some, step.captures.some, res.text, JSON.parse, checkAssert, text.trim, res.headers.get
worker/src/push.ts ↗
  • L23 · b64urlToBytes calls (conditional paths may differ): replace, s.replace, slice, atob, bin.charCodeAt
  • L30 · bytesToB64url calls (conditional paths may differ): String.fromCharCode, replace, btoa
  • L35 · concat calls (conditional paths may differ): out.set
  • L43 · hkdf calls (conditional paths may differ): crypto.subtle.importKey, crypto.subtle.deriveBits
  • L50 · encryptPayload calls (conditional paths may differ): b64urlToBytes, crypto.subtle.generateKey, crypto.subtle.exportKey, crypto.subtle.importKey, crypto.subtle.deriveBits, concat, enc.encode, hkdf, crypto.getRandomValues, crypto.subtle.encrypt
  • L79 · vapidHeader calls (conditional paths may differ): bytesToB64url, enc.encode, JSON.stringify, Math.floor, Date.now, crypto.subtle.importKey, b64urlToBytes, crypto.subtle.sign
  • L95 · sendPush calls (conditional paths may differ): enc.encode, JSON.stringify, Promise.all, subs.map, encryptPayload, vapidHeader, fetch, dead.push
worker/src/tls.ts ↗
  • L11 · getCertExpiry calls (conditional paths may differ): Date.now, connect, socket.writable.getWriter, socket.readable.getReader, setTimeout, reject, Promise.race, writer.write, buildClientHello, drainHandshakeRecords, findLeafCertificate, parseNotAfter, Number.isFinite, reader.read, concat, catch, writer.close, socket.close
  • L51 · concat calls (conditional paths may differ): out.set
  • L67 · drainHandshakeRecords calls (conditional paths may differ): raw.subarray, concat
  • L84 · findLeafCertificate calls (conditional paths may differ): hs.subarray, body.subarray
  • L105 · buildClientHello calls (conditional paths may differ): crypto.getRandomValues, encode, concat, withLen16, len24, len16
  • L154 · withLen16 calls (conditional paths may differ): concat, len16
  • L165 · parseNotAfter calls (conditional paths may differ): reader.expect, reader.readSeqInto, cert.readSeqInto, tbs.peek, tbs.skip, tbs.readSeqInto, validity.skip, validity.readTLV, parseAsn1Time
  • L215 · parseAsn1Time calls (conditional paths may differ): decode, parseInt, s.slice, rest.slice, Date.UTC
Build and deployment pipeline · 1 GitHub Actions workflow

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

CI · .github/workflows/ci.yml ↗

Triggers: push, pull_request

build · no job dependencies declared

  1. actions/checkout@v4actions/checkout@v4
  2. actions/setup-node@v4actions/setup-node@v4
  3. Worker — install & type-checknpm ci npx tsc --noEmit
  4. Dashboard — install, type-check & buildnpm ci npx tsc --noEmit npm run build
package.json ↗
  • build:dashboard: npm --prefix dashboard run build
  • deploy: node scripts/deploy-banner.mjs
worker/package.json ↗
  • deploy: wrangler deploy

Full upstream document by @CalmRay-Solutions · README.md · snapshot 93b2c90

Uptime Guard

Uptime Guard

Uptime monitoring with zero servers. Runs 100% on Cloudflare. Free.

Monitor your websites, APIs, TCP ports, DNS, TLS certificates, domains, cron jobs and multi-step scripted flows - no VPS, no container, no monthly bill. Just a Cloudflare Worker, a database, and a dashboard.

Deploy to Cloudflare

License: MIT Runs free on Cloudflare No server required PRs Welcome

▶ Try the live demo  ·  password demo  ·  see a public status page
Uptime Guard walkthrough

Why Uptime Guard?

Every other uptime tool makes you choose: pay per monitor (UptimeRobot, Pingdom, Better Uptime) or host it on a server you then have to monitor too (Uptime Kuma on a VPS). Uptime Guard is neither. It runs entirely on Cloudflare Workers + D1, so there's nothing to keep alive and, for a typical fleet, nothing to pay.

Uptime Guard SaaS monitors Self-hosted (VPS)
Monthly cost $0 $$ per monitor VPS bill
Server to maintain None - You run it
Who monitors the monitor? Cloudflare's edge vendor 🤷 nobody
Your data Your Cloudflare account vendor's cloud your box
Deploy time ~2 min, one click signup provision + install
Public status page ✅ built-in often paid varies

No server. No monthly bill. No babysitting. Push it to Cloudflare and forget it's there - until it pings you on Telegram because something you care about went down.

Runs on Cloudflare's free tier

The whole thing fits inside Cloudflare's free limits for a real-world fleet:

  • Workers: 100k requests/day free - a per-minute cron is ~1,440/day.
  • D1: 5 GB storage + 5M row-reads/day free - history is rolled up daily and pruned.
  • Static assets: the dashboard is served free from the same Worker.

Dozens of monitors, checked every minute, with 90 days of history - comfortably $0/month.

Features

7 monitor types HTTP(S), TCP port, DNS record, TLS certificate expiry, domain (registrar) expiry, heartbeat (dead-man's switch), and multi-step custom scripts
Alerts that escalate Telegram + desktop Web Push on down/recovery, with widening re-alerts (5m → 10m → 20m → 40m → hourly) and total downtime on recovery
No false alarms A single failed check is re-confirmed in seconds before it ever pages you
Real reliability data Per-service 24h / 7d / 30d / 90d uptime, incident history, and mean-time-to-recovery
Public status pages Share a read-only page per project - no login, edge-cached, sanitized
Installable PWA Add to your desktop/phone; push notifications work even when it's fully closed
Locked down Password + TOTP login, rate-limiting, one-click session revocation
Configure in-app Telegram bot/chat/thread and your authenticator are editable right in Settings
Cloudflare-aware Detects CF-fronted hosts and shows a calm "CF Protected" state instead of false downs

Screenshots

Service detail - SLA windows & MTTR Incident history
Public status page - shareable, no login

Deploy in ~2 minutes

Option A - one click

Deploy to Cloudflare

Cloudflare provisions the D1 database, builds the dashboard, and deploys the Worker. The deploy log ends with your dashboard URL in a banner you can't miss:

=============================================================
| DEPLOYED - OPEN YOUR DASHBOARD AT:                        |
|                                                           |
|   >>  https://uptime-guard.your-name.workers.dev          |
|                                                           |
| First visit walks you through creating the owner account. |
=============================================================

Open it and the first-run wizard walks you through two steps: set an owner password, then pair an authenticator app. Both are required, so the instance is never live with a single factor. No secrets to set, no schema to run - the app initializes itself. (The banner comes from the npm run deploy script; if the deploy step in the Cloudflare setup screen says npx wrangler deploy, change it to npm run deploy to get it.)

Option B - from your terminal
git clone https://github.com/CalmRay-Solutions/uptime-guard.git
cd uptime-guard/worker && npm install && npx wrangler login

# Create the database and paste the printed id into wrangler.toml
npx wrangler d1 create uptime-guard

# Build the dashboard + deploy the Worker in one command
# (prints your dashboard URL in a banner at the end)
cd .. && npm run deploy

Open the printed URL - the first visit shows a "create your account" wizard. Set a password, scan the authenticator QR (keep the key it shows: it is your way back in if you lose the device), then create a project and add your first monitor. Checks start within 60 seconds. The database schema is created automatically, and the session secret is generated for you.

Optional, all from the dashboard's Settings - never required:

  • Re-pair two-factor auth - scan a new QR to move TOTP to another device.
  • Telegram alerts - paste a bot token + chat id (or set TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID).
  • Web Push - add VAPID keys for desktop notifications.

Lost your authenticator? You own the database, so you can clear the pairing and sign in with the password alone until you set up a new one:

npx wrangler d1 execute uptime-guard --remote --command "DELETE FROM settings WHERE key='totp_secret'"

Architecture

┌────────────────────────┐        every 60s        ┌──────────────────┐
│  Cloudflare Worker      │ ── scheduled() cron ──▶ │  runs due checks  │
│  • REST API (/api/*)    │                         │  http/tcp/dns/tls │
│  • serves the dashboard │ ◀── records results ──  │  domain/heartbeat │
│  • Telegram + Web Push  │                         └──────────────────┘
└───────────┬────────────┘
            │ binds
      ┌─────▼─────┐   ┌──────────────────────────────┐
      │  D1 (SQL) │   │  React + Vite dashboard (SPA) │
      │  projects │   │  served from the same Worker  │
      │  services │   │  password + TOTP auth         │
      │  checks   │   └──────────────────────────────┘
      │  incidents│
      │  rollups  │
      └───────────┘
  • worker/ - the Cloudflare Worker: REST API, a scheduled() cron (per-minute checks + a daily rollup/prune job), Telegram + Web Push, and it serves the built dashboard from the assets binding.
  • dashboard/ - a React + Vite SPA (hand-rolled CSS, embedded fonts, inline icons - no runtime CDN). Talks to the Worker over the REST API with a signed session token.

Monitor types

  • HTTP(S) - fetch a URL, assert the status range, optionally a body keyword or a JSON field.
  • TCP Port - open a raw socket to host:port. SSH, Postgres, SMTP, game servers, anything.
  • DNS record - resolve A/AAAA/CNAME/MX/TXT/NS over DNS-over-HTTPS, optionally asserting the value.
  • TLS certificate - parse the served cert's expiry and warn before it lapses. CF-fronted hosts show a non-alerting CF Protected state.
  • Domain expiry - registrar expiry via RDAP, warns before it lapses.
  • Heartbeat - a dead-man's switch: your cron hits a unique /ping/<token> URL each run; a missing ping within the grace window flips it down.
  • Custom script - chain up to 10 requests, pass values between them, assert on each. For flows a single request can't express: log in, then read an authenticated endpoint.
POST https://api.example.com/auth
header content-type: application/json
body {"user":"probe","pass":"..."}
expect status 200
capture token = json data.token

GET https://api.example.com/orders
header authorization: Bearer {{token}}
expect status 200
expect json orders.count > 0
expect time < 800ms

Workers can't eval, so this is a declarative format the Worker interprets - not JavaScript, and nothing arbitrary executes. The first failed assertion marks the monitor down and names the step: step 2 (GET api.example.com/orders): orders.count = 0, expected > 0. Directives: header, body, expect status|time|body|json, capture <name> = json|header|status|body, # comments. A step with no expect status defaults to requiring 2xx, and the whole run shares one timeout.

Security

Password + TOTP login with HMAC-signed sessions, login rate-limiting with a progressive delay and hard lockout, and one-click session revocation. Public status pages expose only names, status and uptime - never targets, IPs, or config. See SECURITY.md.

Limitations & roadmap

  • Single owner - one operator account, no multi-user/teams yet.
  • Runs on Cloudflare - if Cloudflare itself has an outage, checks pause. A redundant external runner is the natural phase 2.
  • Telegram + Web Push today - email / Slack / Discord and maintenance windows are on the roadmap.

PRs for any of these are very welcome - see CONTRIBUTING.md.

License

MIT © CalmRay · ⭐ the repo if this saved you a server.

Frequently asked about Uptime Guard

What is Uptime Guard?+

Uptime Guard is a self-hosted Better Stack/Pingdom alternative built on the Cloudflare developer platform. Uptime checks, escalating alerts and public status pages from one Worker

What does Uptime Guard replace?+

Uptime Guard is listed as an alternative to Better Stack, Pingdom, UptimeRobot. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does Uptime Guard use?+

Uptime Guard is built on D1, Workers.

How much does Uptime Guard cost to run?+

A per-minute cron is about 1,440 Worker invocations a day, inside the Workers Free daily request allowance; D1 history is rolled up and pruned daily. Very large fleets can hit Free plan limits on CPU time and subrequests per invocation. Checks run from Cloudflare’s network on a one-minute cron, not from chosen probe regions. Telegram and Web Push alerts are optional and need their own bot or VAPID keys. The root and worker/ Wrangler files describe the same Worker; the root one exists for the Deploy to Cloudflare button. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Official Cloudflare pricing pages saved 31 Oct 2026 (Markdown editions of developers.cloudflare.com pricing documentation). Check current Cloudflare pricing before deploying.

Is Uptime Guard open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/CalmRay-Solutions/uptime-guard/93b2c904721bd43f9c2366555c99dcee4824f4af/LICENSE. Source code and contributor credit are available at https://github.com/CalmRay-Solutions/uptime-guard.

Community rating

No ratings yet. Tried this project? Share your experience.

One rating per verified account. You can change or remove yours. Accounts are email verified; use of the software is self-reported.

Discuss your experience ↓
Sign in to rate

Discussion · 0

sign in to comment →
No comments yet — be the first.