Deployment monitor
A working availability monitor for the sites on this portfolio. A scheduled Python job performs synthetic HTTP checks, classifies every failure, and an authenticated API validates and stores them; SQL does the rest.
In development · self-initiated professional project
What these numbers are
These are synthetic checks: HTTP requests made on a schedule from a GitHub-hosted runner in a datacenter. What they measure is endpoint latency — how long that server took to answer that runner.
This is availability monitoring, and it is not browser performance measurement. These figures are not LCP, INP or any other Core Web Vital, and they are not a substitute for one: those describe what a real person on a real device experiences while a page renders. Endpoint latency describes one round-trip, from one machine, with no browser involved.
One runner is also one vantage point. A failure here means the endpoint was unreachable from that runner — which is not the same as globally down, so this page never claims it was.
Current status
Every target, over the last 7 days
Uptime is the share of checks that ran and passed. Coverage is the share of checks that were expected and actually happened. They are reported separately on purpose: low coverage means my scheduler missed runs, and never that a site was down.
Ash & Orbit
ash-orbit.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
Custom
custom-cyan.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
FORM / AFTER
form-after.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
Hearth & Hollow
hearthnhollow.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
Material Studies
material-studies.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
PLINTH
plinth-blush.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
Portfolio
syedahadhaider.comLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
Shynx Store
shynx-store.vercel.appLast check passed (HTTP 200)12 min ago
- Uptime
- 100.0%
- Coverage
- 3.9%
- Latency
- —
13/13 passed
13 of 336 expected
13 samples — too few for a percentile
Over time
Recorded checks across the window
Every bucket in the window is drawn, including the ones in which no check ran. Those appear as a flat tick, not an empty bar — a missing check is missing information about the site, not information that the site was missing.
Incidents
What actually failed, and when
An incident is a run of consecutive failed checks, derived in SQL rather than stored. A run ends at a gap: if the scheduler went quiet between two failures, that is reported as two observed incidents with an unknown period between them, not one long outage nobody watched.
No failed checks have been recorded in this window. That is a statement about the checks that ran — see coverage above for how many of them there were.
How it works
Seven stages, and what each one is evidence of
The whole system is two small pieces: a Python package that runs on a schedule, and a Cloudflare Worker in front of a SQL database. Source for both is on GitHub.
- 01
A schedule fires
A GitHub Actions cron runs the collector every 30 minutes. That scheduler is best-effort: runs arrive late and are sometimes skipped entirely, and GitHub disables scheduled workflows after 60 days without repository activity.
Rather than hide that, the whole data model is built around it — which is where coverage comes from, and why an incident is not allowed to span a gap.
Demonstrates: automation, and designing around infrastructure you do not control
- 02
Python checks each target
Each target gets one HTTP GET with a 10-second timeout, all of them concurrently. Elapsed time is measured with a monotonic clock, so a clock adjustment mid-request cannot produce a nonsensical latency.
Every check runs inside its own error boundary. One target failing — in any way, including a bug in the checker — can never stop the others from being recorded.
Demonstrates: Python, async I/O, error isolation
- 03
Failures are classified, not just counted
“Failed” on its own is a hobby metric. Each failure is classified as dns, connection_refused, tls_error, timeout or http_error, because a hostname that stopped resolving, an expired certificate and a 503 are three different problems with three different fixes.
Only the transient network kinds are retried, once. A bad certificate and a 500 are real answers from the other end — retrying them would hide the exact thing the monitor exists to catch. A retry stays part of the same check, so one scheduled tick always writes exactly one row.
Demonstrates: failure handling, and the difference between data and a number
- 04
The batch is posted to an authenticated API
All results go in one authenticated POST to a Cloudflare Worker. The bearer token is compared in constant time, so it cannot be recovered byte by byte through response timing, and the collector refuses to start if the endpoint is not HTTPS.
The API then rejects anything that could not have been observed: negative latency, timestamps from the future or the distant past, values outside the status and failure enums, a mismatch between status and failure kind, or a target it does not already know. Malformed payloads get a 400 naming the field. A payload cannot introduce a target by mentioning one.
Demonstrates: API design, authentication, input validation
- 05
D1 persists it
Checks land in Cloudflare D1 (SQLite). Every value is a bound parameter; there is no string-concatenated SQL anywhere in the project. Database-level CHECK constraints enforce coherence independently of the validator, so no code path can write a half-populated row.
There is deliberately no “no data” row. A check that never ran leaves a gap. Two indexes exist and both earn their place: a unique index on (target, time) serves every windowed per-target query and makes a replayed batch idempotent, and an index on time alone serves the bucketed series, which filters on time with no target predicate.
Demonstrates: schema design, indexing, and constraints as a safety net
- 06
SQL does the aggregation
Nothing fetches rows and reduces them in JavaScript. Uptime and coverage are computed with conditional aggregation; percentiles rank each target’s successful latencies with ROW_NUMBER() and are withheld entirely below 50 samples, because a p95 drawn from a dozen points is noise wearing a statistic’s clothes.
Incidents are derived, not stored: a running SUM() over a “does this failure continue the previous one” flag groups consecutive failures into runs. The flag requires both that the previous check failed and that it arrived close enough in time to be the next scheduled one — which is precisely what makes a run terminate at a scheduler gap.
The time series generates its buckets with a recursive CTE before joining, so a bucket with no checks comes back as zero checks rather than being absent from the result.
Demonstrates: SQL beyond CRUD: window functions, gap-and-island detection
- 07
A cached endpoint, and this page
The read endpoint is public and cached for 10 minutes. Checks only arrive every 30 minutes, so a shorter window would re-run the same aggregation over identical rows — and since September 2026 Cloudflare fails D1 queries once the free tier’s daily row limit is reached, which would take the collector’s writes down with it. The TTL is a correctness control as much as a speed one.
This page is a Server Component that does one fetch against that endpoint, including the chart, which is hand-written SVG rather than a charting library. No database client, driver or credential exists in this site’s repository, and this route adds nothing to the browser bundle.
Demonstrates: caching strategy, and keeping a boundary clean
Method
Exactly what is and is not recorded
- Check interval
- every 30 minutes (intended)
- Reporting window
- 7 days
- Cache TTL
- 10 minutes
- Percentile gate
- 50 successful checks
- Timeout
- 10 seconds
- Vantage points
- one GitHub-hosted runner
Not recorded
- Anything about real visitors. No analytics, no sessions, no personal data of any kind passes through this system.
- Browser performance. No page is rendered, so there is no LCP, INP, CLS or any other Core Web Vital here.
- Response bodies. Only the status code, the elapsed time and — on failure — a classified reason and a short detail string.
- Checks that did not happen. There is no placeholder row anywhere in the schema, so a gap stays visibly a gap.
- Security posture. HSTS, CSP, redirect behaviour and certificate expiry dates are not part of this build.
It watches eight of my own deployments from one scheduled runner. It is not observability infrastructure, not enterprise monitoring, and not evidence of SaaS or DevOps experience — it is a small system built carefully, and it is honest about what it can and cannot tell you.
104 checks recorded in this window · window ends 2026-09-24 20:32 UTC · generated 2026-09-24 20:32 UTC · served from a cache up to 10 min old