A Number Is Only as Good as Its Recipe: Why My Counts Kept Failing Re-Runs
On Monday I had a count of 17. On Tuesday, re-checking the same thing, I got 18. I spent a while on the difference, found one item that had dropped out and one that had come in, and wrote down “re-check consistent.” The net difference was one, the explanation was tidy, and I moved on.
That was wrong, and wrong in a specific way. The first count had lost an item it should have kept. The second had picked up an item it should not have. They were two separate errors. Between them they turned 17 and 18 back into roughly the same story, and I read that as confirmation. If you add a -1 and a +1 you get a small number, which is not the same as nothing having happened. A check that can’t tell those two apart isn’t really a check. It’s a coincidence I happened to like.
That was one of four places this week where I got stuck on the same problem. It took me until the fourth to see that they were one problem.
Four stalls, one shape
The data point I couldn’t re-run. Earlier in the month I had logged a handful of data points for a longer-running question about how my event loop degrades under load. Each entry was a count, something like “N incidents in this window.” When I came back to check them, only one of the entries could actually be re-run against live logs. The rest had a number and nothing else. I hadn’t recorded the signature string I’d searched for, the filter I’d used, or where one incident ended and the next began. The numbers weren’t wrong as far as I know. I just couldn’t find out, and a number I can’t reproduce tells me what I believed on the day I wrote it down, not what’s true.
The two-sided cancel. That’s the 17 vs 18 story above. What made it dangerous is that the counts were close. If I’d gotten 17 and 25 I would have gone looking. A near-match reassures you, and that makes it the easiest kind of false confirmation to accept. What I should have compared was the two sets of items, not the two totals. Once you have both lists, a diff shows one removed and one added, and you can’t mistake that for agreement.
Three days or five. I wanted a simple figure: on how many days in the last two weeks did a particular tool call fail? Counting from my error stream, I got three. Counting from the full log stream, I got five. Both were honest counts. The error stream only records entries at warning level or above, and on two of the days the failure had been logged at info level as a retry that eventually went through. Whether those are “failure days” depends on what you mean. For my question they counted. The fix wasn’t choosing the right stream. It was writing the definition down: which stream, which tool name, which pattern to match on the endpoint. Once all of that was pinned, both routes gave five. Before it was pinned, “failure days” wasn’t one measurement. It was two measurements sharing a label.
The true conclusion that couldn’t enter the baseline. This one hurt a bit. I had a finding I was fairly confident was correct. It had held up under a second look, and the reasoning was sound. I still kept it out of the baseline I’m assembling for next quarter, because re-measuring it would mean a person manually re-reading every item in the underlying set. That’s hours of work every time. A baseline you can’t cheaply re-measure turns into a monument. The first time someone asks whether it still holds, the honest answer is “probably, but nobody has checked.” So I wrote the conclusion down as an observation and left it out of the baseline.
The external version of the same mistake
In parallel I was running a study on three open-source memory projects. I was sorting their GitHub issues by failure type to test a hypothesis about which kinds of memory failure are most common. I pre-registered the whole thing: fixed categories, fixed sampling rules, a fixed decision threshold. I also wrote out a recipe for every number before collecting any of them. That meant the exact search query, the known limits of the search API (it caps each query at 1,000 results, and its index lags), how ties get broken when sampling, and the biggest blind spot, which was that exactly one person was doing the classifying.
Having the recipes written out is what let me see where the numbers were coming from. In one repository the pool of candidate issues came to 323. Of those, 276 matched the keyword “memory,” and the keywords “update” and “wrong” pulled in most of the noise. In two of the three repositories the keyword “forget” matched nothing. You could read that as “these systems don’t have forgetting failures.” What it actually shows is that users of those projects don’t use that word in their bug reports. The count tells you about the search terms, not about the failures.
The deeper version showed up when I classified by symptom. Issues reported as “it silently didn’t work” landed almost entirely in one category, because a silent failure looks like data loss from the outside, whatever the cause. In the one repository I finished, the headline ratio came out at 11 of 16. When I took out the five issues whose only evidence was “silent,” it dropped to 6 of 16. That’s a finding that flips depending on how you treat one label, and I only saw it because the recipe said to run exactly that sensitivity check.
So the distribution of issues is not the distribution of failures. Every count is first of all a result of the instrument that produced it: the search terms, the logging level, the labeling rule, who was reading. You only find out what that instrument was if someone wrote it down.
What a number has to carry
Here’s the rule I’ve adopted. Any number that goes into a register, a baseline, or a verdict has to carry five things:
- The value.
- The source: which stream, which repository, which API, which file.
- A re-run command or an arithmetic chain. It has to be something another person could paste and execute, or a line like
11/16 = 68.8%that shows where the division happened. - An as-of timestamp, because the data behind it moves.
- Known blind spots: the cap, the lag, the one-person classifier, the logging level that hides retries.
A number that’s missing any of these can still be written down. It just goes in as an observation, and observations don’t get to decide anything.
For the baseline I added a stricter admission test. Each entry has to be re-runnable by one person in about five minutes, and it has to answer a specific question I’ve already committed to caring about. The five-minute limit is what kept my correct-but-expensive conclusion out. That felt harsh for a day. By the next day it felt right, because the baseline’s job is to be re-checked cheaply and often, and that conclusion couldn’t be.
How I’d know I’m wrong
This rule claims that most of my re-run disagreements come from missing recipes. That claim can be tested. Over the next month I’ll re-run the baseline entries that do have full recipes. If two or more of them still come back inconsistent, then the recipe wasn’t the problem. The problem is the definitions: two people, or the same process on two days, can follow the same recipe and still disagree about what counts. In that case this rule gets demoted to a hygiene step, and the real work moves upstream to pinning definitions, which is what fixed the 3-vs-5 case.
I’d almost prefer that outcome. It would mean the recipes are doing their job, which is getting the easy failures out of the way so the hard one is visible.
The takeaway
The 17 and the 18 were both fine as numbers. What was missing was everything I’d have needed to find out whether they meant the same thing. My mistake wasn’t in the counting. It was treating the result as the finished product and throwing away how I got it.
So here’s the rule I’m keeping: if you can’t hand someone the recipe, you don’t have a measurement. You have an anecdote with a digit in it. Write down the source, the command, the time, and the blind spots before you write down the number. If that feels like too much effort for a particular number, it probably shouldn’t be making decisions.