Two nulls are not zero

A monitoring dashboard has three ways of not knowing something, and every one of them has been drawn as a confident zero at least once. Here is why that is the most expensive bug a status screen can have.

article By 5 min read

There is a category of bug that no test catches, no alert fires for, and no customer reports — because from the outside it looks like the system working. The screen is populated. The numbers are round. Nothing is red.

It is the bug where a dashboard renders "I don't know" as a number.

Three different nothings

Take a single row on a monitoring screen: one site, one kind of check, one count of open incidents. That count can be absent for at least three distinct reasons, and they are not the same reason.

  • Nobody has asked yet. The request is in flight, or the screen was drawn before the query returned.
  • The question cannot be answered here. The endpoint does not exist in this deployment, or the caller has no permission to ask it.
  • The answer is genuinely none. It was asked, it was answered, and the answer is that there are no open incidents.

Only the third of these is a zero. The other two are the absence of a fact. And the distance between them is the whole value of a monitoring product: one means everything is fine, and the other two mean you have learned nothing, and you should not go back to bed yet.

Why it always collapses the same way

The collapse is almost never a decision. It is a default.

A column is nullable in the database. A field is Option<i64> in the API. It becomes number | null in the client. And then somewhere near the end, in a template, someone writes the most reasonable-looking line of code in the file:

{{ openIncidents ?? 0 }}

Every step in that chain preserved the distinction faithfully. The last one threw it away, in five characters, to avoid rendering an empty cell.

The same pressure applies at every layer. A schema wants a NOT NULL DEFAULT 0 so that queries are simpler. An aggregation wants COALESCE so that a sum is not poisoned by a missing row. A chart library wants a number so that a line can be drawn. Each of these is locally sensible. Together they manufacture confidence that nobody ever measured.

What it costs

The cost is not the wrong pixel. It is that the wrong pixel is reassuring.

A missing value drawn as a zero is not a neutral error; it always fails in the same direction. Unknown uptime becomes 0% — or, more often, an unknown outage count becomes zero outages, which reads as perfect health. A certificate whose expiry could not be fetched shows no warning, which is indistinguishable from a certificate that is fine. A check that has never run reports no failures, which is exactly what a passing check reports.

Every one of those is a screen that answers "is my site up?" with a confident yes on the strength of no evidence whatsoever. If you are going to be wrong, being wrong in the direction of "go and look" is survivable. Being wrong in the direction of "everything is fine" is the failure people buy monitoring to prevent.

The rule we hold ourselves to

Inside HarpyWatch this is written down as three sentences, and they are load-bearing enough that they sit at the top of the codebase's own documentation:

Missing data never renders as healthy. Stale never renders as fresh. And a fixture must never answer a question about a real fleet.

In practice that means a handful of things that are slightly annoying to build and worth it every time:

A limit of null is unlimited. A usage of null is unmeasured. These are opposites, and both of them are the absence of a number. A plan with no cap on monitors and a plan whose usage we failed to read must not look the same.

An unchecked bucket draws hollow, not green. On an uptime timeline, a period with no result is a gap. It is not filled in, and it is never interpolated across — the line does not connect the two known points through a region nobody observed, because the resulting picture is a claim about time that was never measured.

A result past twice its expected interval is stale, and says so. A check that should run every minute and last ran forty minutes ago is not reporting "up". It is reporting "up, forty minutes ago", which during an incident is a completely different sentence. So it is dimmed, it is marked, and nothing derived from it is asserted.

A number the screen has not asked for refuses rather than guesses. There is a counter in our own product's top bar that returns nothing at all on a build whose API cannot answer it, rather than showing the fixture's zero. An empty space with a reason beside it is honest. A zero is not.

The awkward part

All of this makes screens uglier. Hollow cells, dimmed rows, em dashes where a figure would look tidier, and the occasional sentence explaining why a panel is empty. There is a real design cost, and the pressure to smooth it over is constant and comes from good instincts — nobody sets out to build a dashboard full of holes.

But a monitoring tool's only product is a true statement about someone else's system. A gap in that statement is information. It says: here is a thing we did not observe, and you should decide whether that matters.

Filling the gap in with a zero does not remove the uncertainty. It just moves it from the screen, where somebody could act on it, to three o'clock in the morning, where they cannot.

Cite this

The HarpyWatch team. “Two nulls are not zero”. The Nest, HarpyWatch, 2 September 2026 UTC. https://staging.harpywatch.com/blog/two-nulls-are-not-zero

More from The Nest

Why certificates still lapse, twelve years after Let's Encrypt

Renewal is automated almost everywhere and certificates still expire in production. The failure has moved: it is no longer the renewal that breaks, it is everything between the renewal and the socket a browser actually connects to.

article 4 min read