The test failed about once in every fifteen CI runs and never on my machine. It checked the due date printed on an invoice, and every so often it came out a day early. For two weeks the team did what teams do with flaky tests: someone hit retry, it went green, everyone moved on.
On a quiet Thursday I decided to actually find it. I switched the suite to random order locally and ran it in a loop until it failed, which took eleven runs. That gave me a seed. With the seed, the failure was reliable, and that is the moment every developer knows: you've finally got the bug in a jar. I started cutting the test list down, running the suite with the same seed and fewer files each time. After about forty minutes I had it down to the invoice test plus three candidates that ran before it, and a strong suspicion that one of them froze the test clock and never let it go.
Then Slack: "Quick one, do you know where the staging mail password lives?"
I did know. It took me two minutes to find the vault path and paste it. I even felt good about it, because I'd unblocked someone. Then I went back to my terminal and looked at a scrollback full of nearly identical runs, and I could not remember which of the three candidates I had already ruled out. I wasn't sure which run was the last failing one. In trying to get my bearings I reran the suite without the seed flag, got green, ran it again, got green again, and realised what I'd done.
Finding that seed again took most of an hour. The actual fix, once I was back where I'd been, was one line in a tearDown method. A two-minute question had cost me close to fifty minutes, and nobody involved had done anything wrong.
What the question actually erased
Debugging isn't mostly typing. Most of the time goes into building and updating a private model of what's happening: which code path ran, what state was wrong, which assumptions I've already tested and thrown out. That model lives in working memory, and it's rarely written down in full because writing it down feels slower than just carrying on.
When the ping came, the model included the seed, the exact subset of files in the last run, which of the three suspect tests I'd eliminated and how, and a half-formed idea about why the date was off by one only near midnight UTC. None of that was on screen in a form I could read back. The scrollback had the raw output, but the meaning of each run was in my head, and two minutes of thinking about vault paths was enough to dislodge it.
A ten-word answer can wipe a model that took an hour to load.
That's why I don't find generic "it takes X minutes to refocus" claims very useful for this. Getting back into a debugging session is closer to reconstruction than to refocusing, and the cost depends on how much state you were carrying, not on how long the interruption lasted. A ten-word answer can wipe a model that took an hour to load.
Screenshot
Screenshot · 16:9terminal scrollback of repeated invoice test runs, the latest command missing the seed that made the failure reliable, monospace capture, ratio 16:9, screenshot
The person asking isn't being careless
What makes this hard to fix is that the interruption was completely reasonable from the other side. My colleague needed the password to finish their own task. Asking me took them ten seconds, and the cost to me was invisible to them. If they'd known it would cost me fifty minutes, they'd have looked for it themselves or waited an hour.
Teams that never name that asymmetry tend to end up with two kinds of developer. There are the ones who answer everything instantly and quietly lose afternoons, which was me. And there are the ones who ignore every message during focus time and get a reputation for being unhelpful. Neither is a good outcome, and both come from the cost being hidden.
The general version of this problem applies to all kinds of task switching, and the real cost of context switching covers it. Debugging is the worst case because the state is the largest and the least written down.
Knowing when I'm in the fragile zone
Not every bit of debugging is fragile. Reading a stack trace for an obvious null is fine to interrupt. The expensive zone has a recognisable feel, and I've learned to notice it:
I've just reproduced something intermittent and I'm scared to lose it.
I'm stepping through code with a specific hypothesis I'm about to confirm or kill.
I'm narrowing something down by elimination, and the eliminations exist only in my head.
I'm looking at a production issue with incomplete logs and have pieced together a timeline.
I have more than five or six tabs, buffers or terminals all tied to the same thread.
When I notice that, two things change. Chat gets treated like a fire alarm rather than a doorbell, and I start writing state down as I go, which I should have done on that Thursday.
Diagram
Diagram · 16:9a short chain from a reproduced failure, to tests already ruled out, to the next command, the private model a ping knocks out of working memory, boxes and arrows, ratio 16:9, diagram
Writing the run log as I go
The fix that's made the biggest difference is cheap and slightly tedious. When I'm in an elimination-style hunt, I keep a scratch file open beside the terminal and write one line per run. Here's roughly what it looks like for a hunt like the invoice one:
seed 48213, full suite: FAIL (invoice due date 1 day early)
seed 48213, invoice + billing/*: FAIL
seed 48213, invoice + billing/Reminders only: PASS
seed 48213, invoice + billing/Reports only: FAIL
suspect: ReportsTest freezes clock in setUp, check tearDown
It costs maybe ten seconds a run. When something pulls me away, the model is mostly on disk already, and coming back means reading five lines, not reconstructing an hour. If I'd had that file on the Thursday, the Slack question would have cost me the two minutes it took to answer and not much else.
If I have time to prepare, I also set a status before going in: "Holding a repro on the invoice flake until 12:00, P1 only." People are generally happy to respect that if they can see it. The sixty-second park from working at home with interruptions is the same idea for when I have to stand up suddenly: next experiment, failing assertion or request ID, branch and dirty files, the command to rerun.
When the interruption wins anyway
Some days the interruptions win. A production issue elsewhere, a client call that runs long, someone at the door with a parcel for next door. When I come back and realise I've lost the model, I've learned not to pretend otherwise.
The worst thing I can do is carry on poking at it, half-remembering, rerunning things I may have already tried. That's how a lost twenty minutes becomes a lost afternoon. What I do instead is stop for ten or fifteen minutes and write everything I still know into the ticket: the last known good commit, the failing command, what I've ruled out and why, and the next two experiments. Then I treat myself as a new investigator picking up someone else's notes.
Usually I tell the team too, in one line: "Lost my place on the invoice flake, state is in the ticket, picking it up again at 14:00." That has a side benefit I didn't expect. Once the state is written down, someone else can pick it up if they have a free hour, which never happens when the whole thing lives in my head.
What teams can agree on
Individual habits only go so far. The teams I've worked on that handled this well had a few plain agreements:
One channel for genuine emergencies. Production down, data loss, security. Everything else can wait for the next time the person checks messages.
The question goes in the first message. "Got a sec?" forces the other person to stop and reply before they even know if it's urgent. "Where's the staging mail password? No rush, I'll look in the vault meanwhile" lets them decide.
Status lives in written updates. Leads who ping for progress during difficult debugging teach people to hide what they're doing. A short written update at a predictable time, as in writing async status people actually read, removes most of the reason to ask.
Review requests wait too. A review request is an interruption with good intentions. It can sit until the person surfaces.
During real incidents, interruptions are constant and depth is still needed, so the answer is roles. One person holds the model and debugs. One person handles communication with everyone else. One person gathers logs and facts and feeds them to the first person in a single incident thread, not in side messages. Without that split, everyone pings the person debugging with partial information and the model resets every couple of minutes. One rule I've seen work well: nobody asks the person holding the investigation for status unless the incident thread has been checked first.
Pairing can be the exception to all this. When two people are working the same problem with clear roles, both hold the model and an interruption to one doesn't wipe it. A random "can you hop on a call and look at this?" with no context is a different thing entirely. If I pull someone into my debugging, I send the ticket and my run log first and let them read before we talk.
Before you ping someone else
The reverse matters as well. When I'm stuck, I try to write out the hypothesis and what I've tried before asking anyone. Fairly often, writing it down is enough to spot the problem myself, which saves exporting my reset cost to a colleague. When I do ask, I send reproduce steps rather than urgency.
And if it's late, I'm tired, and the model is fragile, I've found it's often better to write a solid park note and stop. Debugging at eleven at night tends to produce sloppy fixes. A rested model with good notes the next morning usually gets there faster.
Protecting debugging time can look antisocial from the outside, and I used to worry it made me seem unhelpful. I don't think that any more. The fix ships when the model survives long enough to change the code with evidence, and the cheapest way to help a teammate who's debugging is usually to look in the vault yourself.
End of this piece
Writer
PrimedEcho
Independent notes on remote work, focus, and freelance practice. About this site.