← All lessons

A Number in Prose Is Not a Measurement

The decision was right. The number wasn’t.

A migration widening a column on a payments table carried a header saying it needed a DBA to apply it by hand rather than going through the automated runner. One of the justifications was size: “~1,488,834 rows — about 15x the ~100k rule of thumb.”

The number came from the schema dump’s AUTO_INCREMENT value. That isn’t a row count. It’s the next id to be issued, and deleted rows leave gaps. Production actually held 782,315 rows — about 8x the rule of thumb, not 15x.

The decision didn’t change. The migration still routed to a DBA, because the justification stood on two independent legs: size, and the fact that a FLOAT → DECIMAL change has no in-place path in MariaDB at any size. Only one leg moved. But that was luck in how the argument was built, not diligence in how the number was found — and the 1.49M figure was already on its way into other documents as fact.

The numbers that weren’t

Once I started checking the numbers in our planning docs, the error rate was humbling:

  • A Kubernetes memory survey reported a median of 7.18 GiB for a container with a 4 GiB limit. That’s physically impossible — the kernel OOM-kills it long before. The cause: cAdvisor emits a separate time series per pod, and when a pod is replaced its series stops updating but stays inside the query window. On that cluster, 1,080 of 1,744 memory series — 62% — belonged to pods that no longer existed. The quantile was dutifully rolling up frozen samples from the dead. Measured against live pods only, the real 7-day peak was 3.32 GiB.
  • Every registration count a project had quoted was inflated by ~39%. The log index also carried a reg-before-env-manip event type, and a match_phrase query for reg matched it because the analyzer tokenized the value into reg | before | env | manip. Every measurement had used the same query, so they all agreed with each other — consistently wrong.
  • An enforcement gap estimated at “about 80 sites” was 22 out of 432 when someone actually enumerated it.
  • A replay-set size moved four times — from 33,000 down to 10,000 — across successive documents, each one citing the last.

None of these came from carelessness. They came from a number being derived once, by one method, written into prose, and then inherited. Once a number is in a doc, it stops being a measurement and becomes a citation.

Why the errors survive

The method fails silently. AUTO_INCREMENT always returns a plausible integer. A Prometheus quantile always returns a value. A text query always returns a count. None of them warn you that they answered a slightly different question.

Consistency masquerades as confirmation. Ten documents agreeing on the registration count felt like ten confirmations. They were one measurement, quoted ten times.

Impossible numbers get rounded into plausible ones. A median above the memory limit should be a fire alarm. In a table of forty containers, it’s just a cell.

The rule

The rule that came out of this is simple enough to write on a sticky note: derive any number that drives a decision twice, by independent methods. Not the same query run twice — two different paths to the same quantity.

  • Row count: COUNT(*) on the replica, and the table’s statistics, and a sanity check against the id range.
  • Memory usage: the time-series quantile, and the live pods’ current working set, and a check that no value exceeds the limit.
  • Event counts: the text query, and an exact-term query on the keyword field, and a spot check of distinct values.

If the two methods agree, you’ve earned the number. If they disagree, you’ve found something more valuable than the number — usually a misunderstanding about what one of your tools actually measures.

A few habits that help

Write the method next to the number. “782,315 (prod, COUNT(*), 2026-09-21)” is a measurement. “~780K rows” is a rumor with a decimal point.

Check against physical bounds. A percentile above a hard limit, a count above the id range, a rate above the request rate — these are free checks, and they catch the worst errors.

Correct in place, loudly. When we found the AUTO_INCREMENT mistake, the migration file was corrected and the changelog said so explicitly, including that the conclusion survived. A quiet fix lets the old number live on in every doc that already copied it.

Build arguments on more than one leg. The column migration survived its bad number because the decision didn’t rest on that number alone. You won’t always have two independent reasons — but when a decision rests on exactly one number, that number deserves two independent derivations.

The principle

A number is only as good as the method that produced it, and prose strips the method off. Before you act on a figure — especially one you didn’t measure yourself — ask how it was derived, and derive it again a different way. It’s an hour of work. Acting on a number that’s off by 2x, or by “physically impossible,” costs a lot more than that.

← All lessons