
isUpMap
An internet-service status heatmap with incident history and community reports
isUpMap is a self-hosted Downdetector alternative built on Cloudflare (Analytics Engine, D1, KV, Queues, Workers). Paid services required. 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
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Paid services required
Budget Workers Paid from $5 USD/account/month for the documented 80+service polling fan-out: Worker Free allows only 50 external subrequests per invocation. D1, KV and vote-queue quotas also apply. Analytics Engine currently has a Free allowance and is not billed, according to its current primary pricing page; optional maps have independent limits.
Hosting requirements
- Provide a strong VOTE_SALT, your own D1/KV/Queue resources and a production route; workers.dev is disabled in the committed config.
- Optional Protomaps maps and GA4 tracking are disabled without their settings. The captured deployment has not been executed or load-tested.
- Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
Sources checked 01/10/2026
Repository snapshot: 0b78646. Hosting eligibility reflects the deployment documentation and listed assumptions.
- downdetector ↗
A live **up / down heatmap** for 80+ popular internet services, rendered as a stock-map style treemap with brand logos. Built on a single [Cloudflare Worker](https://developers.cloudflare.com/workers/): a **Cron Trigger** polls each service's official status and persists snapshots + an incident log to **D1**, and a static frontend renders the treemap with per-service uptime, an incident history, a command palette, light/dark themes, and a mobile-friendly detail sheet.
- workers ↗
//developers.cloudflare.com/workers/wrangler/configuration/ */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "cf-isupordown", "main": "src/index.ts", "compatibility_date": "2026-05-30", "assets": { // The path to the directory containing the `index.html` file to be served at `/` "directory": "./public", // Expose assets to the Worker so it can rewrite /analytics.js at the edge. "binding": "ASSETS", // Run the Worker first for this one path so it can inject the GA4
- d1 ↗
database_id automatically. // For manual deploys: run `npx wrangler d1 create isupmap` and paste the id here. "d1_databases": [ { "binding": "DB", "database_name": "isupmap", "database_id": "" }, // Separate D1 for community vote data — isolated so vote storms during an // outage don't serialise behind the authoritative status writes in DB. // Create: `npx wrangler d1 create isupmap-reports` and paste the id below. { "binding": "REPORTS_DB", "database_name": "isupmap-
- kv ↗
nd the /api/* routes read it, so reads never // touch D1. Replace the placeholder id below with your own — run // `npx wrangler kv namespace create SNAPSHOT_KV` and paste the id. Local dev // and tests simulate KV regardless of the id value. "kv_namespaces": [ { "binding": "SNAPSHOT_KV", "id": "00000000000000000000000000000000" } ], // Queue for buffering community vote writes. The consumer coalesces a burst // of votes into a handful of D1 inserts so the vote storm during an outage // do
- queues ↗
directly from the edge. // Create: `npx wrangler queues create isupmap-votes` (+ isupmap-votes-dlq). "queues": { "producers": [ { "binding": "VOTE_QUEUE", "queue": "isupmap-votes" } ], "consumers": [ { "queue": "isupmap-votes", "max_batch_size": 100, "max_batch_timeout": 60, "max_retries": 3, "dead_letter_queue": "isupmap-votes-dlq" } ] }, // Per-IP rate limiting for the public /api/* endpoints: 60 requests / 60s. // VOTE_RATE_LIMITER is tighter (10 req / 60 s) t
- analytics-engine ↗
via the AE SQL API. // No setup needed — the dataset is created automatically on the first write. "analytics_engine_datasets": [ { "binding": "FETCH_ANALYTICS", "dataset": "isupmap_fetches" } ], "workers_dev": false, "preview_urls": false }
- paid ↗
/** * For more details on how to configure Wrangler, refer to: * https://developers.cloudflare.com/workers/wrangler/configuration/ */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "cf-isupordown", "main": "src/index.ts", "compatibility_date": "2026-05-30", "assets": { // The path to the directory containing the `index.html` file to be served at `/` "directory": "./public", // Expose assets to the Worker so it can rewrite /analytics.js at the edge. "binding": "ASSETS", // Run the Worker first for this one path so it can inject the GA4 ID from // the GA_ID var; every other asset is still served directly (asset-first). "run_worker_first": [ "/analytics.js" ] }, // Google Analytics 4 measurement ID, injected into /analytics.js at the edge. // Em
- paid ↗
The Workers Paid plan includes Workers, Pages Functions, Workers KV, Hyperdrive, and Durable Objects usage for a minimum charge of $5 USD per month for an account. The plan includes increased initial usage allotments, with clear charges for usage that exceeds the base plan. There are no additional charges for data transfer (egress) or throughput (bandwidth).
- paid ↗
| Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |
- paid ↗
| Keys read | 100,000 / day | 10 million/month, + $0.50/million |
- paid ↗
| Standard operations | 10,000 operations/day included | 1,000,000 operations/month included + $0.40/million operations |
- paid ↗
| **Workers Paid** | 10 million included per month <br> (+$0.25 per additional million) | 1 million included per month (+$1.00 per additional million) |
- paid ↗
| [Subrequests](#subrequests) | 50/request | 10,000/request |
- paid ↗
Currently, you will not be billed for your use of Workers Analytics Engine. Pricing information here is shared in advance, so that you can estimate what your costs will be once Cloudflare starts billing for usage in the coming months.
- paid ↗
| Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows |
- paid ↗
| Keys written | 1,000 / day | 1 million/month, + $5.00/million |
- MIT ↗
MIT License Copyright (c) 2026 Jairon Landa 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 CONNEC
- architecture ↗
/** * For more details on how to configure Wrangler, refer to: * https://developers.cloudflare.com/workers/wrangler/configuration/ */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "cf-isupordown", "main": "src/index.ts", "compatibility_date": "2026-05-30", "assets": { // The path to the directory containing the `index.html` file to be served at `/` "directory": "./public", // Expose assets to the Worker so it can rewrite /analytics.js at the edge. "binding": "ASSETS", // Run the Worker first for this one path so it can inject the GA4 ID from // the GA_ID var; every other asset is still served directly (asset-first). "run_worker_first": [ "/analytics.js" ] }, // Google Analytics 4 measurement ID, injected into /analytics.js at the edge. // Em
- architecture ↗
//developers.cloudflare.com/workers/wrangler/configuration/ */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "cf-isupordown", "main": "src/index.ts", "compatibility_date": "2026-05-30", "assets": { // The path to the directory containing the `index.html` file to be served at `/` "directory": "./public", // Expose assets to the Worker so it can rewrite /analytics.js at the edge. "binding": "ASSETS", // Run the Worker first for this one path so it can inject the GA4
- architecture ↗
database_id automatically. // For manual deploys: run `npx wrangler d1 create isupmap` and paste the id here. "d1_databases": [ { "binding": "DB", "database_name": "isupmap", "database_id": "" }, // Separate D1 for community vote data — isolated so vote storms during an // outage don't serialise behind the authoritative status writes in DB. // Create: `npx wrangler d1 create isupmap-reports` and paste the id below. { "binding": "REPORTS_DB", "database_name": "isupmap-
- architecture ↗
nd the /api/* routes read it, so reads never // touch D1. Replace the placeholder id below with your own — run // `npx wrangler kv namespace create SNAPSHOT_KV` and paste the id. Local dev // and tests simulate KV regardless of the id value. "kv_namespaces": [ { "binding": "SNAPSHOT_KV", "id": "00000000000000000000000000000000" } ], // Queue for buffering community vote writes. The consumer coalesces a burst // of votes into a handful of D1 inserts so the vote storm during an outage // do
- architecture ↗
directly from the edge. // Create: `npx wrangler queues create isupmap-votes` (+ isupmap-votes-dlq). "queues": { "producers": [ { "binding": "VOTE_QUEUE", "queue": "isupmap-votes" } ], "consumers": [ { "queue": "isupmap-votes", "max_batch_size": 100, "max_batch_timeout": 60, "max_retries": 3, "dead_letter_queue": "isupmap-votes-dlq" } ] }, // Per-IP rate limiting for the public /api/* endpoints: 60 requests / 60s. // VOTE_RATE_LIMITER is tighter (10 req / 60 s) t
- architecture ↗
via the AE SQL API. // No setup needed — the dataset is created automatically on the first write. "analytics_engine_datasets": [ { "binding": "FETCH_ANALYTICS", "dataset": "isupmap_fetches" } ], "workers_dev": false, "preview_urls": false }
Documented public demo screenshot · Jaironlanda/isupmap repository contributors; screenshot captured by Cloudsteading ↗. 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.
A service-status overview, incident history and community reports; differences in source coverage and outage-detection methodology mean it is not a like-for-like Downdetector replacement.
See supporting source ↗How it works
The shape of isUpMap 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 ↗Configuration and workflow sources
Reviewed commit 0b78646b79dc. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 1 files
Cloudflare Workers · compatibility 2026-05-30
cf-isupordown · default
Entrypoint: src/index.ts
Static assets: ./public · Worker first: ["/analytics.js"]
Cron triggers (UTC): */5 * * * *
DB→ D1REPORTS_DB→ D1SNAPSHOT_KV→ KVFETCH_ANALYTICS→ Analytics EngineVOTE_QUEUE→ Queues (producer) · queue isupmap-votesisupmap-votes→ Queues (consumer) · queue isupmap-votes · dead letters isupmap-votes-dlqASSETS→ 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.
- L299 · fetch handler exported · references GA_ID, ASSETS, DB, VOTE_RATE_LIMITER, VOTE_SALT, SNAPSHOT_KV, VOTE_QUEUE, REPORTS_DB, PROTOMAPS_KEY · calls env.ASSETS.fetch, asset.text, source.replace, JSON.stringify, toString, cache.match, rateLimit, loadSnapshot, json, staleness, ctx.waitUntil, cache.put, resp.clone, kickColdStart, url.searchParams.get, summarize, shieldsBadge, Math.min, Math.max, Number, findService, recentIncidents, url.pathname.match, dailyUptime, days.reduce, request.headers.get, env.VOTE_RATE_LIMITER.limit, request.json, console.error, normalizeReason, countryOf, Date.now, hashIp, Math.floor, env.SNAPSHOT_KV.get, env.VOTE_QUEUE.send, env.SNAPSHOT_KV.put, readCount, aggregateReports, writeCount, markup, renderIncidentFeed, renderSitemap, Response.redirect, renderStatusIndex, renderNotFound, snapshot.services.find, Boolean, catch, renderServicePage
- L577 · scheduled handler exported · references DB, REPORTS_DB · calls refreshSnapshot, when.getUTCHours, when.getUTCMinutes, Promise.all, pruneIncidents, pruneReports, console.log, console.error, String
- L597 · queue handler exported · references REPORTS_DB, SNAPSHOT_KV · calls map, insertReports, rows.map, Promise.all, touched.map, aggregateReports, writeCount, console.error, String, batch.retryAll
- L33 · json calls (conditional paths may differ): JSON.stringify
- L79 · resolveAll calls (conditional paths may differ): Promise.allSettled, SERVICES.map, resolveStatus, settled.map
- L104 · staleness calls (conditional paths may differ): Date.now, Math.max
- L111 · warmingSnapshot calls (conditional paths may differ): SERVICES.map
- L133 · decorateDisabled calls (conditional paths may differ): DISABLED_REASONS.get
- L151 · loadSnapshot calls (conditional paths may differ): env.SNAPSHOT_KV.get, warmingSnapshot, snapshot.services.map, known.has, snapshot.services.push, decorateDisabled
- L173 · refreshSnapshot calls (conditional paths may differ): resolveAll, persistSnapshot, readSnapshot, decorateReports, env.SNAPSHOT_KV.put, JSON.stringify
- L190 · decorateReports calls (conditional paths may differ): detectSurges, reportSparklines, surges.get, sparks.get, spark.some, console.error
- L214 · kickColdStart calls (conditional paths may differ): ctx.waitUntil, finally, catch, refreshSnapshot, console.error
- L260 · summarize calls (conditional paths may differ): parts.push, parts.join
- L291 · rateLimit calls (conditional paths may differ): request.headers.get, env.API_RATE_LIMITER.limit, json
Environment references: env.SNAPSHOT_KV · env.FETCH_ANALYTICS · env.DB · env.REPORTS_DB · env.API_RATE_LIMITER · env.GA_ID · env.ASSETS · env.VOTE_RATE_LIMITER · env.VOTE_SALT · env.VOTE_QUEUE · env.PROTOMAPS_KEY
- L115 · persistSnapshot calls (conditional paths may differ): Date.now, db.batch, db.prepare, map, state.get, batch.push, upsertState.bind, prevCurrent.get, confirmStatus, JSON.stringify, upsertCurrent.bind, open.get, isIncident, openIncident.bind, closeIncident.bind, updateIncident.bind, bind, String
- L189 · uptimeFraction calls (conditional paths may differ): Math.max, Math.min
- L206 · readSnapshot calls (conditional paths may differ): Date.now, db.batch, db.prepare, bind, byService.get, list.push, byService.set, currentRows.map, safeParse, uptimeFraction, Number
- L269 · dailyUptime calls (conditional paths may differ): Date.now, Math.floor, all, bind, db.prepare, Math.min, Math.max, result.push, slice, toISOString
- L305 · recentIncidents calls (conditional paths may differ): all, bind, db.prepare, rows.results.map
- L340 · pruneIncidents calls (conditional paths may differ): Date.now, run, bind, db.prepare
- L345 · safeParse calls (conditional paths may differ): JSON.parse
- L29 · findService calls (conditional paths may differ): SERVICES.find
- L33 · escapeHtml calls (conditional paths may differ): replace, value.replace
- L74 · formatUpdated calls (conditional paths may differ): toLocaleString
- L106 · layout calls (conditional paths may differ): escapeHtml, JSON.stringify
- L187 · formatPercent calls (conditional paths may differ): Math.round
- L192 · formatDay calls (conditional paths may differ): toLocaleString
- L200 · renderUptimeCard calls (conditional paths may differ): history.reduce, join, history.map, formatDay, formatPercent
- L228 · renderServicePage calls (conditional paths may differ): copy.sentence, escapeHtml, formatUpdated, statusPageUrl, renderUptimeCard, layout
- L372 · renderStatusIndex calls (conditional paths may differ): byCategory.get, list.push, byCategory.set, join, map, byCategory.entries, services.sort, a.name.localeCompare, escapeHtml, layout
- L413 · renderNotFound calls (conditional paths may differ): layout
- L429 · renderSitemap calls (conditional paths may differ): SERVICES.map, join, urls.map
- L445 · formatDuration calls (conditional paths may differ): Math.max, Math.round, Math.floor
- L464 · renderIncidentFeed calls (conditional paths may differ): Date.now, incidents.reduce, Math.max, join, incidents.map, formatDuration, formatUpdated, escapeHtml, toISOString
- L103 · normalizeReason calls (conditional paths may differ): VALID_REASONS.has
- L122 · insertReports calls (conditional paths may differ): db.prepare, db.batch, rows.map, stmt.bind, Math.floor
- L167 · aggregateReports calls (conditional paths may differ): Date.now, Math.floor, db.batch, bind, db.prepare, map, countries.reduce, timeline.push, dayCounts.get, hourly.push, hourCounts.get
- L237 · reportSparklines calls (conditional paths may differ): Date.now, Math.floor, all, bind, db.prepare, byService.get, hours.set, byService.set, arr.push, hours.get, out.set
- L264 · pruneReports calls (conditional paths may differ): Date.now, run, bind, db.prepare
- L336 · detectSurges calls (conditional paths may differ): Date.now, db.batch, bind, db.prepare, map, base.keys, counts.keys, counts.get, base.get, writes.push, upsert.bind, Math.max, Math.sqrt, out.set
- L406 · hashIp calls (conditional paths may differ): encoder.encode, crypto.subtle.digest, join, map, Array.from, padStart, b.toString
- L418 · readCount calls (conditional paths may differ): kv.get
- L423 · writeCount calls (conditional paths may differ): kv.put, JSON.stringify
- L44 · fetchUpstream calls (conditional paths may differ): setTimeout, controller.abort, fetch, clearTimeout
- L71 · fetchWithRetry calls (conditional paths may differ): Date.now, fetchUpstream, logFetch, delay, String
- L148 · fetchStatuspage calls (conditional paths may differ): fetchWithRetry, base.replace, result, res.json, filter, map, components.filter, statuspageStatus
- L188 · nodeText calls (conditional paths may differ): String
- L195 · latestEntry calls (conditional paths may differ): Array.isArray, Date.parse, Number.isNaN, nodeText, title.trim, String
- L252 · stageSignal calls (conditional paths may differ): description.replace, plain.matchAll, stages.add, toLowerCase, stages.has, DOWN_RE.test
- L272 · leadingSignal calls (conditional paths may differ): re.exec, at, Math.min, DOWN_RE.test
- L298 · classifyEntry calls (conditional paths may differ): exec, DOWN_RE.test, stageSignal, leadingSignal, RESOLVED_RE.test, ACTIVE_RE.test
- L334 · fetchRss calls (conditional paths may differ): fetchWithRetry, result, latestEntry, xmlParser.parse, res.text, Date.now, toISOString, classifyEntry
- L375 · fetchHttp calls (conditional paths may differ): Date.now, fetchWithRetry, result, BOT_WALL_CODES.has
- L396 · resolveStatus calls (conditional paths may differ): result, fetchStatuspage, fetchRss, fetchHttp
- L26 · logFetch calls (conditional paths may differ): ae.writeDataPoint
Build and deployment pipeline · 1 GitHub Actions workflows
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.
Triggers: push, pull_request
check · no job dependencies declared
- actions/checkout@v7
actions/checkout@v7 - actions/setup-node@v7
actions/setup-node@v7 - Shell command
npm ci - Shell command
npm run types - Shell command
npm run typecheck - Shell command
npm test
deploy: wrangler deploy && wrangler d1 execute isupmap --remote --file=./schema.sql
Repository README
View original on GitHub ↗Full upstream document by @Jaironlanda · README.md · snapshot 0b78646
isUpMap — Service Status Heatmap
Live: isUpMap (Official) Live: map.warmindex.com (Backup)
A live up / down heatmap for 80+ popular internet services, rendered as a stock-map style treemap with brand logos. Built on a single Cloudflare Worker: a Cron Trigger polls each service's official status and persists snapshots + an incident log to D1, and a static frontend renders the treemap with per-service uptime, an incident history, a command palette, light/dark themes, and a mobile-friendly detail sheet.

Features
- Treemap heatmap — services grouped into category "sectors", tiles sized by prominence and colored by status, each with a brand logo.
- Per-service detail — tap a tile (mobile) or click it (desktop) for status, 24h/7d uptime, component rollup, active incident, status-page host, and a "Visit status page" link.
- Incident history — server-recorded incidents (open/close/duration), persisted in D1 and shown in a side panel and per-service (last 2 shown).
- 90-day uptime history — Statuspage-style daily bars (colored by each day's
worst incident status, with per-day uptime tooltips) plus the window average,
derived from the incident log. Shown in the detail modal and on the SSR
/status/<id>pages. - Community "report it's down" — users can vote a service is down with a
reason (
Can't connect,Errors,Can't log in,Slow,Other). Votes are deduplicated per IP-hash per 24 h, buffered through a Cloudflare Queue, and persisted in a separate D1. The per-service detail page shows aggregate counts, a 24-hour report-volume chart (Downdetector-style line + faint area over a shaded "typical range" band), per-country and per-reason breakdowns, and a Protomaps world map of recent report origins. - Report-surge detection — a Downdetector-style anomaly signal layered on the
community reports. Each cron run scores a service's recent report volume against
its own rolling baseline (EWMA mean + variance) and raises a surge when the
count is a statistical outlier (≥3σ), clears an absolute floor (≥5 reports), and
holds for 2 consecutive passes. A surging service is shown as a new "Reported"
status color (orange) and a 24h sparkline on big tiles — supplementary only,
it never opens an incident or alters the authoritative probe status. See
detectSurges. - Command palette (⌘K) — fuzzy-search services and run commands.
- Local customization — show/hide services or categories and a
"problems only" filter, persisted in
localStorage. - Notifications — in-page toasts on status changes, plus optional opt-in desktop notifications.
- Atom incident feeds — subscribe to outages in any feed reader (or an
RSS-to-alert tool):
/feed.xmlcovers every service,/status/<id>/feed.xmlcovers one. Entries update in place when an incident resolves ("Resolved: GitHub was down for 25m"). - Light / dark theme, deep links (
?service=,?filter=problems), and a favicon/title that reflect overall state.
How it works
Cron (every 5m) ─▶ scheduled() ─▶ resolveStatus() × N ┌─▶ Statuspage JSON ({base}/api/v2/summary.json)
│ (1 retry on a blip) ├─▶ RSS / Atom feed (fast-xml-parser)
│ (src/sources.ts) ────────────────┘
│ └─▶ HTTP reachability ping
│ each fetch attempt ──────────────▶ Analytics Engine (FETCH_ANALYTICS)
│ (service id, url, latency, status code, attempt)
├─▶ persist to D1 (src/db.ts): flap-dampen → upsert `current`,
│ open/close `incidents`; daily retention sweep
├─▶ decorateReports() — surge detection + 24h sparkline from REPORTS_DB
│ (supplementary; failure never blocks the status snapshot)
└─▶ publish finished snapshot to KV (SNAPSHOT_KV)
Browser (public/) ─poll /api/status every 45s─▶ Worker reads KV (Cache-API fronted) ─▶ treemap + uptime
└─open detail / panel ───────▶ GET /api/incidents (D1, cached) ─▶ incident history
Community reports:
User taps "Report it's down" ─▶ POST /api/report/:id (VOTE_RATE_LIMITER: 10/60s per IP)
│ hash IP → VOTE_QUEUE (Cloudflare Queue)
└─▶ queue consumer → batch-insert into REPORTS_DB (D1)
GET /api/report/:id ─▶ KV hit (10-min TTL) → or D1 aggregate query → report JSON
(total, countries, reasons,
recent 10, 7-day timeline,
24h hourly series, surge flag)
/status/:id page ─▶ server-rendered with Protomaps world map (report locations) + analytics panel
- A Cron Trigger (
*/5 * * * *) invokesscheduled(), which resolves every service concurrently (Promise.allSettled, each upstream guarded by a 15s timeout + edge caching, with one retry on a transient blip so a flaky connection doesn't read asunknown). It persists the result to D1, then publishes the finished snapshot to KV for the read path. - Flap-dampening (
src/db.ts) — a non-upstatus must hold for 2 consecutive polls before it is committed tocurrentor opens an incident; recovery toupis immediate, and anunknown(timed-out) probe holds the last status rather than resolving a real incident. A single glitchy upstream read therefore never paints a false outage. Resolved incidents older than 90 days are pruned once a day (~03:00 UTC). GET /api/statusis a fast KV read of the snapshot the cron publishes (no D1 query on the hot path), with per-service uptime (24h / 7d). It's fronted by the Cache API; before the first cron run it serves a "warming" snapshot (never a live fan-out). The response carries astaleflag (older than 3 cron cycles) so the UI warns instead of showing frozen data. Each service also carries a supplementarysurgeflag and a 24h report-volumesparkseries (added by the cron) that the UI uses for the "Reported" tile color and per-tile sparkline.GET /api/incidentsreturns the recent incident log from D1 (Cache-API fronted). An optional?service=<id>filter scopes it to a single service.GET /api/uptime/:idreturns a 90-day daily uptime series for one service ({ date, uptime, worst }per UTC day, oldest first, plus auptime90window average), computed from incident intervals —degradedcounts against uptime, matching the 24h/7d numbers. Same Cache-API + rate-limit fronting as/api/incidents(5-min TTL). Approximations: days before a service was added read 100%, and an incident whose severity changed carries its final status for the whole interval.GET /feed.xml(and per-serviceGET /status/<id>/feed.xml) serves the same incident log as an Atom feed for feed readers and RSS-to-alert tools. Entry ids are permanent and<updated>moves on resolution, so readers show recovery as an update to the same item rather than a duplicate. The dashboard and SSR pages advertise the feeds via<link rel="alternate">autodiscovery. Cache-API fronted with a 5-min edge TTL (the cron cadence), so aggressive polling never reaches D1.POST /api/report/:idaccepts a community down-report ({ reason }) for a known service. The IP is hashed withVOTE_SALT(SHA-256; never stored raw), deduplicated per IP-hash per 24 h, and enqueued toVOTE_QUEUEfor async batch-insert intoREPORTS_DB. Tighter rate limit: 10 req / 60s per IP (VOTE_RATE_LIMITER). Returns503ifVOTE_SALTis not set.GET /api/report/:idreturns aggregated community-report data for a service: 7-day total, per-country and per-reason breakdowns, up to 10 most-recent individual reports, a 7-day daily volume timeline, a 24h hourly series (for the report-volume chart), and the current surge flag. Cached inSNAPSHOT_KV(reportcount:<id>) with a 10-min TTL.GET /api/summaryreturns a single overall-status rollup (worst status wins) plus a headline ("All systems operational"/"2 down, 1 degraded"), per-status counts, and the samestaleflag — handy for compact embeds, badges, or a status banner. Add?format=shieldsfor a shields.io endpoint badge payload (status-colored), which powers the live badge at the top of this README. Same KV-read + Cache-API fronting as/api/status.- All API routes are rate-limited per IP (60 req / 60s) via a Workers rate-limit binding.
- Crawlable pages — the dashboard is a client-rendered SPA, so for SEO the
Worker also server-renders (no-JS)
GET /status/<id>(per-service status + uptime + community report summary + Protomaps world map) andGET /status(a directory), plus a generated/sitemap.xmland a staticrobots.txt. These give crawlers real content for queries like "is GitHub down?". See src/pages.ts. The map and report widget assets are only shipped when the service actually has community report data (showMapflag). - The frontend (public/app.js) lays services out with a
squarified treemap; tiles are sized by a per-service
weight(layout only).
Status model
Every service is normalized to one of: up · degraded · down · unknown.
| Source type | How status is derived |
|---|---|
| Statuspage | Reads {base}/api/v2/summary.json (Atlassian). status.indicator maps: none→up, minor/maintenance→degraded. A major/critical indicator is tempered by breadth — since the indicator reflects peak component severity, not how much is affected — so it only reads down when the major outage is broad (≥50% of components, or too few components to judge); a localized one (e.g. a single region) → degraded. The summary also yields component rollups and active incidents for the detail view. |
| RSS / Atom | Parses the latest feed entry (via fast-xml-parser). Entries older than 48h are treated as resolved (up). A fresh entry mentioning resolved/restored/operational → up; outage/down/major/critical → down; anything else fresh → degraded. Heuristic. |
| HTTP ping | A plain GET. 2xx/3xx → up; other response → degraded; network error or timeout → down. |
| (any) | A failed fetch or timeout → unknown (treated as "no data" — never opens an incident or counts as downtime). |
Statuspage JSON is authoritative where available. RSS-based status is a best-effort heuristic, since incident feeds describe history rather than a current-state field.
There is also a display-only fifth state, reported (orange): shown when
the probe says up/unknown but community reports are surging (see
Report-surge detection). It's derived at render time from the surge
flag and never replaces a service's real status — a confirmed down/degraded
always wins, and incidents and uptime stay 100% probe-driven.
Data sources
All data comes from each service's own public status page / feed. IsUpMap is a
read-only aggregator and is not affiliated with any of these services. The list
lives in src/services.ts; for Statuspage entries the
/api/v2/summary.json path is appended to the base shown below.
Rows marked ⊘ disabled have an upstream that no longer publishes a usable
machine-readable feed, so their status can't be resolved. They are shown as
permanently unknown (grey), hidden from the heatmap, and appear locked in the
Customize panel with the reason on hover — see
Disabled services below.
| Service | Category | Source | Endpoint / base |
|---|---|---|---|
| GitHub | Developer & Cloud | Statuspage | https://www.githubstatus.com |
| Cloudflare | Developer & Cloud | Statuspage | https://www.cloudflarestatus.com |
| npm | Developer & Cloud | Statuspage | https://status.npmjs.org |
| DigitalOcean | Developer & Cloud | Statuspage | https://status.digitalocean.com |
| Vercel | Developer & Cloud | Statuspage | https://www.vercel-status.com |
| Netlify | Developer & Cloud | Statuspage | https://www.netlifystatus.com |
| MongoDB | Developer & Cloud | Statuspage | https://status.mongodb.com |
| Sentry | Developer & Cloud | Statuspage | https://status.sentry.io |
| CircleCI | Developer & Cloud | Statuspage | https://status.circleci.com |
| Linode | Developer & Cloud | Statuspage | https://status.linode.com |
| Render | Developer & Cloud | Statuspage | https://status.render.com |
| AWS | Developer & Cloud | RSS | https://status.aws.amazon.com/rss/all.rss |
| Google Cloud | Developer & Cloud | RSS | https://status.cloud.google.com/en/feed.atom |
| Microsoft Azure | Developer & Cloud | RSS · ⊘ disabled | https://azure.status.microsoft/en-us/status/feed/ |
| Supabase | Developer & Cloud | Statuspage | https://status.supabase.com |
| Fly.io | Developer & Cloud | Statuspage | https://status.flyio.net |
| Railway | Developer & Cloud | RSS · ⊘ disabled | https://railway.betteruptime.com/feed.rss |
| Neon | Developer & Cloud | RSS | https://neonstatus.com/pages/6878fc85709daa75be6c7e3c/rss |
| PlanetScale | Developer & Cloud | Statuspage | https://www.planetscalestatus.com |
| Bunny.net | Developer & Cloud | Statuspage | https://status.bunny.net |
| Auth0 | Developer & Cloud | Statuspage · ⊘ disabled | https://auth0.statuspage.io |
| Clerk | Developer & Cloud | Statuspage | https://status.clerk.com |
| HashiCorp | Developer & Cloud | Statuspage | https://status.hashicorp.com |
| Snowflake | Developer & Cloud | Statuspage | https://status.snowflake.com |
| Elastic | Developer & Cloud | Statuspage | https://status.elastic.co |
| New Relic | Developer & Cloud | Statuspage | https://status.newrelic.com |
| Grafana | Developer & Cloud | Statuspage | https://status.grafana.com |
| PagerDuty | Developer & Cloud | RSS · ⊘ disabled | https://status.pagerduty.com/history.rss |
| Algolia | Developer & Cloud | RSS · ⊘ disabled | https://status.algolia.com/history.rss |
| GitLab | Developer & Cloud | RSS | https://status.gitlab.com/pages/5b36dc6502d06804c08349f7/rss |
| Docker | Developer & Cloud | RSS | https://www.dockerstatus.com/pages/533c6539221ae15e3f000031/rss |
| Appwrite | Developer & Cloud | RSS | https://status.appwrite.online/feed.rss |
| Firebase | Developer & Cloud | RSS (Atom) | https://status.firebase.google.com/en/feed.atom |
| Firecrawl | Developer & Cloud | RSS | https://status.firecrawl.dev/feed.rss |
| OpenAI | AI | RSS | https://status.openai.com/feed.rss |
| Anthropic | AI | Statuspage | https://status.claude.com |
| xAI | AI | RSS | https://status.x.ai/feed.xml |
| Groq | AI | Statuspage | https://groqstatus.com |
| ElevenLabs | AI | Statuspage | https://status.elevenlabs.io |
| Cohere | AI | Statuspage | https://status.cohere.com |
| Replicate | AI | Statuspage | https://www.replicatestatus.com |
| Pinecone | AI | Statuspage | https://status.pinecone.io |
| Runway | AI | Statuspage | https://status.runwayml.com |
| Hugging Face | AI | RSS | https://status.huggingface.co/feed.rss |
| Together AI | AI | RSS | https://status.together.ai/feed.rss |
| Perplexity | AI | RSS | https://status.perplexity.com/default/history.rss |
| Stability AI | AI | Statuspage | https://status.stability.ai |
| Deepgram | AI | Statuspage | https://status.deepgram.com |
| AssemblyAI | AI | Statuspage | https://status.assemblyai.com |
| Cursor | AI | RSS | https://status.cursor.com/history.rss |
| Cerebras | AI | Statuspage | https://status.cerebras.ai |
| Fireworks AI | AI | RSS | https://status.fireworks.ai/feed.rss |
| DeepSeek | AI | Statuspage | https://status.deepseek.com |
| Mistral AI | AI | HTTP ping | https://status.mistral.ai |
| Stripe | Payments | Statuspage | https://www.stripestatus.com |
| Coinbase | Payments | Statuspage | https://status.coinbase.com |
| Shopify | Payments | Statuspage | https://www.shopifystatus.com |
| Plaid | Payments | Statuspage | https://status.plaid.com |
| Paddle | Payments | Statuspage | https://paddlestatus.com |
| Lemon Squeezy | Payments | RSS · ⊘ disabled | https://ohdear.app/status-page/lemon-squeezy-status/subscribe-rss |
| Square | Payments | Statuspage | https://www.issquareup.com |
| Klarna | Payments | Statuspage | https://status.klarna.com |
| PayPal | Payments | RSS | https://www.paypal-status.com/feed/rss |
| Discord | Communication | Statuspage | https://discordstatus.com |
| Slack | Communication | RSS | https://slack-status.com/feed/rss |
| Zoom | Communication | Statuspage | https://www.zoomstatus.com |
| Twilio | Communication | Statuspage | https://status.twilio.com |
| SendGrid | Communication | Statuspage | https://status.sendgrid.com |
| Resend | Communication | Statuspage | https://resend-status.com |
| Mailgun | Communication | Statuspage | https://status.mailgun.com |
| Intercom | Communication | Statuspage | https://www.intercomstatus.com |
| HubSpot | Communication | Statuspage | https://status.hubspot.com |
| Atlassian | Productivity & Media | Statuspage | https://status.atlassian.com |
| Dropbox | Productivity & Media | Statuspage | https://status.dropbox.com |
| Datadog | Productivity & Media | Statuspage | https://status.datadoghq.com |
| Figma | Productivity & Media | Statuspage | https://status.figma.com |
| Box | Productivity & Media | Statuspage | https://status.box.com |
| Squarespace | Productivity & Media | Statuspage | https://status.squarespace.com |
| Wikipedia | Productivity & Media | HTTP ping | https://www.wikipedia.org |
| Linear | Productivity & Media | Statuspage | https://linearstatus.com |
| Notion | Productivity & Media | Statuspage | https://www.notion-status.com |
| Cloudinary | Productivity & Media | Statuspage | https://status.cloudinary.com |
| Asana | Productivity & Media | Statuspage | https://status.asana.com |
| Airtable | Productivity & Media | Statuspage | https://status.airtable.com |
| Miro | Productivity & Media | Statuspage | https://status.miro.com |
| Canva | Productivity & Media | Statuspage | https://www.canvastatus.com |
| Webflow | Productivity & Media | Statuspage | https://status.webflow.com |
| DocuSign | Productivity & Media | Statuspage | https://status.docusign.com |
| Twitch | Gaming & Entertainment | Statuspage | https://status.twitch.com |
| Epic Games | Gaming & Entertainment | Statuspage | https://status.epicgames.com |
| Netflix | Gaming & Entertainment | HTTP ping | https://www.netflix.com |
| Roblox | Gaming & Entertainment | RSS | https://status.roblox.com/pages/59db90dbcdeb2f04dadcf16d/rss |
| Steam | Gaming & Entertainment | HTTP ping | https://store.steampowered.com |
| PlayStation Network | Gaming & Entertainment | HTTP ping | https://www.playstation.com |
| Riot Games | Gaming & Entertainment | HTTP ping | https://www.riotgames.com |
| Spotify | Gaming & Entertainment | HTTP ping | https://open.spotify.com |
| X (Twitter) | Social Media | HTTP ping | https://x.com |
| Social Media | HTTP ping | https://www.facebook.com |
|
| Social Media | HTTP ping | https://www.instagram.com |
|
| YouTube | Social Media | HTTP ping | https://www.youtube.com |
| TikTok | Social Media | HTTP ping | https://www.tiktok.com |
| Social Media | HTTP ping | https://www.whatsapp.com |
|
| Social Media | HTTP ping | https://www.linkedin.com |
|
| Telegram | Social Media | HTTP ping | https://telegram.org |
| Social Media | Statuspage | https://www.redditstatus.com |
|
| Social Media | HTTP ping | https://www.pinterest.com |
|
| Snapchat | Social Media | HTTP ping | https://www.snapchat.com |
| Bluesky | Social Media | HTTP ping | https://bsky.app |
| Threads | Social Media | HTTP ping | https://www.threads.net |
| Mastodon | Social Media | HTTP ping | https://mastodon.social |
| Tumblr | Social Media | HTTP ping | https://www.tumblr.com |
Disabled services
A service is disabled when its upstream no longer provides a status feed we can
trust — it stopped publishing a machine-readable feed, or the feed reports stale /
incorrect state. Rather than show a stale or misleading status, a disabled service is set to
unknown (grey), kept out of the heatmap, and listed — locked and labelled
disabled, with the reason on hover — in the Customize panel. The resolver
skips disabled services entirely (no fetch). To disable one, add a disabled: "<reason>" string to its entry in src/services.ts.
Currently disabled (upstream no longer exposes a machine-readable feed, verified 2026-06-05):
| Service | Reason |
|---|---|
| Microsoft Azure | Global status feed publishes no machine-readable incident items (empty channel). |
| Auth0 | Statuspage JSON (auth0.statuspage.io) is stale — reports a phantom "minor outage" with no active incident while the live page shows all operational. |
| Railway | Migrated to a JS-rendered status page (status.railway.com); the old Betterstack feed is empty. |
| PagerDuty | history.rss now returns an HTML page instead of XML. |
| Algolia | history.rss now returns an HTML page instead of XML. |
| Lemon Squeezy | The Oh Dear feed URL now serves an HTML subscribe page instead of a feed. |
To add a service, append an entry to src/services.ts with its
category, a weight (tile size), and a source. Brand logos are
self-hosted: add the service id to LOGO_DOMAIN (public/app.js)
and drop a matching <id>.png (a ~128px favicon) in
public/images/logo/services/.
Tips: Statuspage hosts often
302-redirect to a canonical domain (e.g.status.zoom.us→www.zoomstatus.com) — use the canonical host to avoid an extra hop. Some hosts (e.g.status.x.ai) put the JSON behind a Cloudflare bot challenge; use their RSS feed instead.
Security
- Rate limiting —
/api/*is capped at 60 requests / 60s per IP (API_RATE_LIMITER); the vote endpoint (POST /api/report/:id) has its own tighter cap of 10 requests / 60s per IP (VOTE_RATE_LIMITER); over-limit returns429. - IP privacy — the vote path never stores raw IPs. The IP is SHA-256-hashed
with a
VOTE_SALTsecret before storage; without a real salt the endpoint returns503rather than persisting a reversible hash. - Vote deduplication — one vote per IP-hash per service per 24-hour bucket (enforced by a D1 primary key), so burst-voting during an outage doesn't inflate counts.
- Caching —
/api/status,/api/summary,/api/incidents, and/api/report/:idare wrapped in the Cache API or KV; D1 stays off the hot read path for all of these. - Headers / CSP — public/_headers sets a strict
Content-Security-Policy(no inline scripts),X-Content-Type-Options,X-Frame-Options,Referrer-Policy, andPermissions-Policyon static assets; the Worker addsnosniff+Referrer-Policyto API responses. The/status/:idpage uses a relaxed CSP (script-src 'self') only when the report widget and map are present; all other server-rendered pages usescript-src 'none'.
Analytics
Frontend (GA4)
Google Analytics (GA4) is wired in public/analytics.js
and loads only in production — it's gated to non-localhost hosts (so it's
off during wrangler dev) and injected from a same-origin module to avoid an
inline script under the CSP. Update or remove GA_ID there to change it.
Upstream fetch analytics (Workers Analytics Engine)
Every upstream fetch attempt made by the cron is logged to a Workers
Analytics Engine dataset (isupmap_fetches) via the FETCH_ANALYTICS
binding (see src/analytics.ts). This gives a
queryable record of each HTTP call the Worker makes — useful for spotting slow
status pages, tracking retry rates, and debugging unknown spikes.
Schema (query via wrangler analytics-engine sql or the AE SQL API):
| Column | Type | Description |
|---|---|---|
timestamp |
auto | When the fetch was made |
index1 |
string | Service ID (use in WHERE / GROUP BY) |
blob1 |
string | URL fetched |
blob2 |
string | Source type: statuspage · rss · http |
blob3 |
string | Error message (empty string on success) |
double1 |
number | HTTP status code (0 for network / timeout errors) |
double2 |
number | Round-trip latency in milliseconds |
double3 |
number | Attempt index (0 = first try, 1 = retry) |
double4 |
number | 1 if the response was ok (2xx), 0 otherwise |
Example queries:
-- Average latency per service over the last hour
SELECT index1 AS service, avg(double2) AS avg_latency_ms, count() AS fetches
FROM isupmap_fetches
WHERE timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY service ORDER BY avg_latency_ms DESC;
-- Retry rate per source type
SELECT blob2 AS source_type,
sum(double3) AS retries,
count() AS total,
round(sum(double3) / count() * 100, 1) AS retry_pct
FROM isupmap_fetches
WHERE timestamp > NOW() - INTERVAL '24' HOUR
GROUP BY source_type;
-- Services with the most errors in the last day
SELECT index1 AS service, count() AS errors
FROM isupmap_fetches
WHERE double4 = 0 AND timestamp > NOW() - INTERVAL '24' HOUR
GROUP BY service ORDER BY errors DESC LIMIT 20;
Development
| Command | Purpose |
|---|---|
npm install |
Install dependencies (fast-xml-parser, wrangler). |
npm run db:schema:local |
Apply schema.sql to the local status D1 (run once before first dev). |
npm run db:schema:reports:local |
Apply schema-reports.sql to the local reports D1. |
npm run dev |
Local dev server at http://localhost:8787. |
npm run dev:cron |
Dev server with --test-scheduled so the cron can be triggered locally. |
npm run types |
Regenerate Worker types (wrangler types) after editing wrangler.jsonc. |
npm run typecheck |
tsc --noEmit. |
npm run deploy |
Deploy to Cloudflare. |
Trigger the cron and inspect the API locally:
npm run dev:cron
curl "http://localhost:8787/__scheduled" # runs scheduled() once → persists to D1 + publishes to KV
curl -s http://localhost:8787/api/status | jq # served from KV, includes per-service uptime + stale flag
curl -s http://localhost:8787/api/incidents | jq
curl -s http://localhost:8787/api/summary | jq # overall status rollup + headline
curl -s http://localhost:8787/api/uptime/github | jq # 90-day daily uptime series
curl -s http://localhost:8787/feed.xml # Atom feed of the incident log
Local cron note: under plain
wrangler dev, the documented/cdn-cgi/handler/scheduledtest route currently throws aDataCloneError: ... ScheduledController(a wrangler local-shim bug). Usenpm run dev:cron(--test-scheduled) and the/__scheduledroute instead. Production crons invokescheduled()natively and are unaffected.
Deploying
Deploy Button (recommended)
Click the button at the top of this README. Cloudflare will fork the repo, provision a D1 database, and deploy — no manual config needed.
After deploy, attach a custom domain / route in the Cloudflare dashboard (or add a
routesentry towrangler.jsonc), sinceworkers_dev: falsedisables the*.workers.devURL.
Manual deploy
npx wrangler d1 create isupmap # status DB; paste the printed id into wrangler.jsonc
npx wrangler d1 create isupmap-reports # community reports DB; paste the id into wrangler.jsonc
npx wrangler kv namespace create SNAPSHOT_KV # snapshot cache; paste the id into wrangler.jsonc
npx wrangler queues create isupmap-votes # vote queue
npx wrangler queues create isupmap-votes-dlq # dead-letter queue
wrangler secret put VOTE_SALT # random string; protects IP hashes from reversal
npm run deploy # deploys the Worker + applies schema.sql to the remote D1
npm run db:schema:reports:remote # applies schema-reports.sql to REPORTS_DB
Re-run
npm run db:schema:remote/db:schema:reports:remoteafter adding indexes/tables (CREATE … IF NOT EXISTSis idempotent).
Set
PROTOMAPS_KEY(viawrangler secret putor inwrangler.jsonc) to enable the world map on/status/:idpages. A free key is available at protomaps.com. Leave it empty to skip the map.
Rate-limit binding
wrangler.jsonc declares a rate-limit binding with namespace_id: 1001. This
number is an arbitrary local identifier — you do not need to create or change it;
Cloudflare provisions the binding automatically on deploy.
Project structure
public/ Static frontend (served directly by Cloudflare)
index.html Page shell: header/toolbar, treemap, panels, detail modal, palette
styles.css Treemap palette, hover card, panels, command palette, bottom sheet, themes
app.js Poll loop, treemap, logos, detail sheet, palette, customization, toasts
report.js Community report widget + Protomaps world map (loaded on /status/:id only)
report.css Styles for the report widget and world-map layout
analytics.js GA4 loader, production-only
robots.txt Crawl rules + sitemap pointer
_headers Security headers / CSP for static assets
images/ OG image + self-hosted service icons (logo/services/<id>.png)
lib/ Vendored MapLibre GL and Protomaps basemap assets (world map rendering)
src/
index.ts Worker entry: scheduled() cron + rate-limited/cached /api/* + SSR /status pages, /sitemap.xml & Atom /feed.xml
services.ts Curated service list + status data sources + shared types
sources.ts Per-source-type fetch + normalize (Statuspage/RSS/HTTP); logs each attempt to AE
analytics.ts logFetch() helper — writes per-attempt fetch events to the Analytics Engine dataset
db.ts D1 persistence: flap-dampening, snapshot upserts, incident transitions, uptime, retention
pages.ts Server-rendered status pages (/status, /status/<id>) + sitemap.xml
reports.ts Community report logic: write/read/aggregate, 24h sparkline + surge detection (REPORTS_DB + SNAPSHOT_KV cache)
schema.sql D1 schema (current / incidents / meta / probe_state) + indexes
schema-reports.sql D1 schema for REPORTS_DB (reports + report_baseline tables + index)
wrangler.jsonc Worker config (main, assets, cron, D1 × 2, KV, Queues, rate limits × 2, Analytics Engine)
Notes & limitations
- Persistence lives in two D1 databases.
DB(status): thecurrentsnapshot (one row per service), anincidentslog (one row per non-upepisode), and ametarow for the last run. Uptime is derived from incident intervals — no high-volume per-poll table — and incident queries are index-backed.REPORTS_DB(community reports): one row per vote (service_id, ip_hash, bucketPK); isolated so vote storms during an outage don't compete with status writes. - Queue (
VOTE_QUEUE) — vote POSTs are enqueued and flushed in batches (up to 100 messages, 60s max wait) by the consumer, so a burst of reports during an outage resolves to a handful of D1 inserts rather than one per request. - History horizons — resolved incidents are pruned after 90 days
(
RETENTION_MSin src/db.ts); community report rows after 30 days (RETENTION_MSin src/reports.ts); raise either for longer history. - Granularity — status/uptime update every 5 minutes (the cron cadence); the UI polls every 45s and serves cached data in between.
- RSS status is heuristic (see the status-model table).
cf: { cacheTtl }edge caching and the Cache API are no-ops/limited underwrangler dev, so local runs hit upstreams live on each cache miss; production caches aggressively. The native rate limiter is eventually-consistent and per-location, so its cutoff is best-effort rather than an exact count.
Sponsors
Thanks to our sponsors for supporting this project:
| Sponsor | Description | |
|---|---|---|
| StashSync.app | Your second brain. Public when you want it. | |
| JSONsilo.com | Host JSON Files with Unmatched Efficiency | |
| WarmIndex.com | Web App Directory for Side Projects & Open Source | |
| UtilsFor.dev | Tools for developers |
Frequently asked about isUpMap
What is isUpMap?+
isUpMap is a self-hosted Downdetector alternative built on the Cloudflare developer platform. An internet-service status heatmap with incident history and community reports
What does isUpMap replace?+
isUpMap is listed as an alternative to Downdetector. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does isUpMap use?+
isUpMap is built on Analytics Engine, D1, KV, Queues, Workers.
How much does isUpMap cost to run?+
Budget Workers Paid from $5 USD/account/month for the documented 80+service polling fan-out: Worker Free allows only 50 external subrequests per invocation. D1, KV and vote-queue quotas also apply. Analytics Engine currently has a Free allowance and is not billed, according to its current primary pricing page; optional maps have independent limits. Provide a strong VOTE_SALT, your own D1/KV/Queue resources and a production route; workers.dev is disabled in the committed config. Optional Protomaps maps and GA4 tracking are disabled without their settings. The captured deployment has not been executed or load-tested. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Check current Cloudflare pricing before deploying.
Is isUpMap open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/Jaironlanda/isupmap/0b78646b79dc7a635f0aae98a942b5003322eff6/LICENSE. Source code and contributor credit are available at https://github.com/Jaironlanda/isupmap.

Discussion · 0
sign in to comment →