Designing for the 2am Debugger: What Your Change Looks Like When You're Asleep
Photo: New Brunswick / Nouveau-Brunswick, Public domain, via Wikimedia Commons
Picture it. It's 01:47 on a wet Tuesday and the person on call has been woken by a pager for the second time this week. They didn't write the code that's misbehaving. The author is asleep, possibly in a different timezone, and definitely not answering Slack. Everything they have to work with is what the change left behind: log lines, names, error messages, and a runbook someone wrote in a hurry before a bank holiday.
That scenario is the real acceptance test for a production change, and it's worth running in your head before you merge. Not does it work — you've got CI for that — but could a tired stranger work out why it's broken from the wreckage?
The test is a habit, not a gate
This isn't a call for a heavyweight checklist or a new column in Jira. It's a two-minute mental exercise: imagine the least-context-rich engineer on your rota, at the worst hour, with the least patience. Then read your own diff as if you were them.
Craft, in this sense, isn't about elegance for its own sake. It's about consideration for the next person. The next person might be a colleague, a contractor, or you in six months when you've forgotten everything except roughly where the file lives.
Log lines are documentation with a deadline
Most logs are written for the happy path, where nobody reads them. The ones that matter are written for the bad day.
A line that says Processing… earns nothing. A line that says invoice_reconcile_failed invoice_id=8842 attempt=3 reason=timeout upstream=billing-api lets the on-call engineer search, correlate and move on. Same number of keystrokes, wildly different value.
A few ground rules that pay off repeatedly:
- Log at the boundaries. Inbound request, outbound call, decision taken. Not every loop iteration.
- Include the identifiers a person would type into a dashboard. Order IDs, tenant IDs, correlation IDs — the things support will quote at you.
- Say what you did about it.
retry_scheduled wait_ms=2000beatsretryevery time. - Never log the secrets, and be genuinely careful with personal data — the ICO is not a theoretical concern for a UK team handling customer records.
- Prefer one good line to twenty vague ones. Volume is not observability; noise pushes the useful line off the screen.
Names carry the night shift
Half of debugging is vocabulary. If the function is called handle_data, the on-call engineer has to open it and read it to learn anything. If it's called reconcileInvoiceLines, they already know what it's for and roughly what breaks when it fails.
The same goes for variables with units. timeout is a riddle. timeout_ms is an answer. Somewhere in your organisation there is a bug caused by someone assuming seconds. You can prevent the next one.
Error messages deserve the same treatment. Invalid input tells nobody anything. quantity must be a positive integer, got -3 tells them exactly where to look, and often tells the user what to do differently.
Design the failure, not just the success
The 2am debugger benefits far more from a well-considered failure mode than from a beautifully factored happy path.
Ask yourself, before merging:
- What happens when the upstream call times out? Do we fail open or closed, and is that the right choice for this feature?
- If we retry, is the operation idempotent? A retry without an idempotency key is how you turn one duplicate charge into three.
- Is there backoff with jitter, or are we about to hammer a struggling dependency with perfectly synchronised enthusiasm?
- Does the failure surface as a clear error, or as a null that quietly poisons something three layers up?
None of this needs a framework. It needs ten minutes of thinking about the bad day while it's still someone else's problem.
Runbooks that survive contact with reality
A runbook nobody can find is decoration. Keep it next to the code, or link it from the alert — not in a wiki page three clicks from anywhere, last edited in 2023.
What actually helps:
- One screen. If it's longer, it's a document, not a runbook.
- The first five minutes. How do you confirm this is the problem? Which dashboard, which query?
- The escape hatch. How do you roll back, and what breaks if you do?
- The escalation. Who to wake, and at what point it's reasonable to wake them.
- A date and a name. Undated runbooks are folklore.
Release habits for a team of six
None of the above survives a team that ships in heroic Friday-afternoon batches. Small, boring habits do more than any process document:
- Ship on a Tuesday or Wednesday where you can. A change deployed at 16:55 on the Friday before a bank holiday is a decision you've made about someone's weekend.
- Keep batches small. A five-file release is diagnosable. A fifty-file release is archaeology.
- Put feature flags in front of anything risky, so the fix is a toggle rather than a hotfix at 3am.
- Note who's on call in the pull request. It's a thirty-second sanity check that changes how you write the description.
- Hand over properly. A short note on Friday — what shipped, what's wobbly, what to watch — is worth more than a heroic postmortem on Monday.
The quiet payoff
The 2am test doesn't make incidents disappear. It shortens them. It turns a two-hour scramble involving three people and a guess into a fifteen-minute look at a log line that says exactly what went wrong.
That's the whole trick: write the code as though the person debugging it is tired, unfamiliar and slightly annoyed — because one day, that person will be you.