Uptime monitoring
HTTP checks from outside your application, how a failure is confirmed before anyone is paged, and why a missing check is never counted as downtime.
An uptime monitor watches one endpoint from outside your application and tells you when it stops answering — without telling you so often that you stop reading. Checks run from one location in the EU.
Name: Checkout API
URL: https://api.example.com/health
Method: GET
Interval: 60 seconds
Timeout: 10 seconds
Expected status: 200-299
Intervals
30, 60, 300 and 600 seconds. A fixed set rather than a free number: arbitrary intervals
make capacity unanswerable and nobody has ever needed 47 seconds. Checks are spread
deterministically across the interval, so a thousand monitors do not all fire on the same second.
What a check does
- An HTTP
GETorHEAD, with a timeout from 1 to 60 seconds (10 by default). - One pooled HTTP client, identified as
ErrorDock-Uptime/1.xso an endpoint owner can tell our traffic from an attack in one log line. Cache-Control: no-cache— a monitor that got a304from an intermediary would be reporting on a cache rather than on your origin.- The body is read to completion up to 1 MiB and discarded. It is never stored and never inspected. Draining it is what makes "response time" mean time to a complete response rather than time to the first packet.
A check succeeds when DNS resolves, the connection succeeds, TLS succeeds where applicable, the response arrives inside the timeout, and the status is inside the expected range.
dns_time, connect_time, tls_time and ttfb are recorded only when actually observed, and
are null otherwise. A check that reused a pooled connection performed no DNS lookup; writing zero
there would corrupt a percentile somebody later makes a capacity decision from.
Failure types
Classification comes from what the socket layer observed, never from parsing an exception message — the same fault produces four different strings across platforms.
| Type | Meaning |
|---|---|
dns_failure |
The hostname did not resolve |
connection_refused |
Something listening, nothing serving |
connection_timeout |
The TCP handshake never completed |
request_timeout |
Connected, but no response inside the timeout |
tls_error |
Handshake failed — chain, protocol or hostname |
certificate_expired |
TLS failed specifically on validity dates |
invalid_status_code |
Answered, outside the expected range |
network_error |
Unreachable, reset, or a socket fault none of the above |
blocked_address |
ErrorDock refused: the target resolves somewhere private |
too_many_redirects |
More than five hops, or a hop to a non-HTTP scheme |
blocked_address is its own type because it is not your endpoint failing — it is ErrorDock
refusing, and the two need different messages.
Incidents
A failure is confirmed before anybody hears about it. The default is 3 consecutive failures, and the retries happen inside one scheduled slot two seconds apart rather than across intervals: a single dropped packet must not page anybody, but confirmation that waits three full intervals announces a real outage three minutes late.
Recovery takes 2 consecutive successes. Without that, an endpoint alternating between 200 and 503 every minute would send a recovery mail and a down mail every two minutes — the behaviour that gets an alert channel muted permanently, including for the outage that matters.
One outage produces one incident, enforced by a unique index rather than by the code that happens to create them. And the incident's start is the instant of the first failed check, backdated by the confirmation delay — a monitor with a threshold of three did not start being down when ErrorDock became sure.
Notifications
Uptime reuses the alert channels you already configured — email, a webhook signed with
X-ErrorDock-Signature, Slack and Discord — and writes an entry to the in-app bell. Down, recovery
and certificate warnings all go that way. The deduplication key is the incident id
rather than a time window, so a six-hour outage produces one down notification and one recovery
notification, whatever happened in between.
Checkout API is down
URL: https://api.example.com/health
Started: 14:32:14
Failure: HTTP 503
Previous: 182 ms
Project: Acme
The previous response time is the most useful line on it: "it was answering in 182 ms a minute ago" and "it has been degrading for an hour" are different incidents.
A recovery for an outage nobody was told about is suppressed — otherwise the first mail you receive is "recovered" about something you never heard was broken. The in-app entry is written even when a monitor has no channels at all.
Uptime calculation
uptime% = successes / (successes + failures) × 100
over scheduled checks that actually happened inside the window. Four consequences:
- A check that did not happen is not a success and not a failure. The answer carries expected and observed check counts, and the difference is reported as a coverage gap.
- No checks means
null, never 100. A monitor created a minute ago has unknown 30-day uptime, not perfect 30-day uptime, and the UI renders it as a dash. - Confirmation retries do not count. Attempts two and three are diagnosis inside one slot; counting them would make one flaky minute weigh three times as much as one solidly dead minute.
- Paused periods are a coverage gap, not an exclusion.
TLS certificate expiry
The leaf certificate is observed during the handshake the check was performing anyway, so this costs nothing extra. Warnings fire at 30, 14 and 7 days, and only the tightest threshold crossed is used — a certificate first seen with five days left produces the 7-day warning once, not all three at once. A renewed certificate is a different expiry instant, so its 30-day warning is a new warning.
The certificate check never overrides .NET's own validation verdict. A monitoring product that accepted invalid certificates in order to keep reporting "up" would be lying about the one thing you most need to know.
Why a customer URL is safe to fetch
Every target is resolved and checked against private, loopback, link-local and cloud-metadata ranges before a connection is made, and again on every redirect hop — resolving once and connecting later is the gap that makes a DNS-rebinding attack work. No role, including owner, may point a monitor at an internal address: the guard is a server-side boundary rather than a permission.
When ErrorDock is the thing that is down
Nothing is ever written for a check that did not happen. Not a success, not a failure, nothing.
- A monitor that has never been checked is
unknown, notup. - A window with no checks has
nulluptime and coverageunknown. - Chart buckets with no checks are holes, not zeroes.
- A monitor that is overdue is caught up to the next slot rather than replayed, so the missed period stays a gap instead of being retroactively filled with invented results.
"We cannot answer" and "nothing went wrong" must never render the same.
Not built
TCP, UDP, DNS and ICMP monitors; keyword and content assertions; POST bodies; domain expiry; browser and synthetic monitoring; checks from more than one location; public status pages; maintenance windows; SMS. The storage is shaped so each can be added without redesigning it, and none of them exists today.
Last updated October 20, 2026