I recently spent days on a Bluetooth problem that I had budgeted an afternoon for. The headset kept dropping when I walked away from the Mac. I assumed it was range. It was not range. By the time I understood what was going on, I had tested power classes, multipoint behaviour, and codecs, and the fix turned out to be architectural.
That was not a badly estimated bug. It was an unestimable one. I have come to think that all bugs are.
Features have a shape. Bugs don’t.
When you estimate a feature, you are estimating work you can see. You know roughly which files change, which parts are boring, and which parts are risky. You will be wrong by a factor, but it is a factor.
A bug is different. A bug is a wrong assumption somewhere in the system, and the difficulty of the bug is the depth of that assumption. Is it in your code? In a library? In the OS? In hardware? In a spec that two vendors read differently? You cannot know until you find it, and finding it is the work. So any estimate you give up front is an estimate of how deep you hope the assumption is.
This is why “how long will this take to fix” is such an uncomfortable question. The honest answer is “I’ll know when I’ve found it,” which sounds like dodging but is just true.
Timebox, don’t estimate
Since the estimate is fiction, I have stopped giving one. What I do instead is set a box.
An hour to reproduce. If I cannot reproduce it in an hour, that itself is a finding, and I go and gather more evidence before I touch any code. Then a box for the first layer, then the next. At the end of each box I write down what I ruled out. If the boxes keep running out, that is information too: the assumption is deeper than I thought, and the question becomes whether it is worth going deeper or whether I should route around it.
Routing around is underrated. The Bluetooth fix was not “make the Mac’s radio reach the bathroom.” It was “put a cheap Raspberry Pi where the Mac’s radio can’t reach.” I only saw that option after enough boxes had failed that I stopped trying to fix the layer I started on.
Drill in a fixed order
The thing I wish I had built years ago is a drill. Not a tool exactly, more a fixed sequence of layers to check so that I do not skip the boring ones because I have a theory.
Theories are the enemy here. When I have a theory, I check the layer the theory points at, and I skip the three layers above it. Then I spend a day proving my theory wrong when a five-minute look at the top layer would have shown the real cause. A drill removes the choice. Process state, then network, then config, then code, in that order, every time, even when I am sure.
A small habit that came out of this: whenever I log into a Linux box, I look at the running processes before anything else. Not because I expect a problem. Because the times I found the problem there, I had not expected it either.
Make it hard to break, so the search is short
The other half of this is upstream. If you cannot predict how hard a bug will be, the best investment is to shrink the space where it can hide.
That means invariants that fail loudly instead of silently. Tests that pin down behaviour so that the wrong assumption is caught at the layer where it was made, not three layers away. Fewer moving parts, because every part is a place an assumption can live. None of this makes bugs predictable. It makes the drill shorter, which is the only lever you actually have.
So the next time someone asks how long a bug will take, I am going to say what I actually know. I can tell you how long I will look before I report back. I cannot tell you what I will find.