…
…
…”
}
I’ll join blocks with
(escaped as literal backslash-n in the JSON source). Writing it out.
Also note: within HTML I have curly braces in “any two of {three or more occurrences, …}” â fine in HTML/JSON.
Let me write the final JSON now. I’ll be meticulous with escaping.
Actually, one more consideration on “harden” the JSON: avoid stray newlines inside the JSON string. I’ll write the entire JSON as a single line? That could be very long but valid. Or multi-line JSON where the string is one line. I’ll write the JSON object across lines but keep the html string on one line (with
escapes inside). That’s valid JSON.
Let me compose.{“title”:”Code Fix or Operational Workaround? A Decision Test for Recurring Failures”,”html”:”
When the same failure shows up for the third time in a quarter, a team of five engineers faces a real decision: change the code, or write a runbook step and move on. The short answer we use at Gray Haven Lab: fix the code when the failure is frequent, customer-visible, or quietly eroding your detection. Keep the workaround when the failure is rare, cheap to absorb, or the fix would be larger than the failure it prevents. This article is the decision test we run â five questions, concrete thresholds, and the carry-cost math that keeps the debate under ten minutes.
Two definitions so the test stays unambiguous. A code fix is any reviewed, tested change: application code, a Terraform module, a Kubernetes manifest, a database migration. An operational workaround is a documented response that lives outside the deploy path: a runbook entry, a remediation script, an alert with attached steps, a manual procedure with a named owner. Neither is inherently better. The question is which one the failure pattern has earned.

Why small teams default to the workaround â and why that is mostly fine
In a 2â15 person team, engineer-hours are the scarcest resource, and every code fix competes directly with the roadmap. A workaround costs minutes per occurrence; a fix costs hours or days up front. Defaulting to the workaround is rational capacity management, not laziness.
But workarounds carry costs that never show up in a sprint plan: attention fragmentation for whoever is on call, alert fatigue when the same page arrives with no resolution in sight, and tribal knowledge when the workaround lives in one person’s head instead of a runbook. The failure mode is not choosing a workaround â it is choosing one silently, without an owner, a counter, or a review date.
The five-question decision test
Run this in the incident review, not during the outage. It takes ten minutes with your incident history open in PagerDuty, incident.io, or a plain incidents.md file in the repo. We describe the full runbook-first incident response setup elsewhere; this test slots into the same review.
1. How often does it actually happen?
Count occurrences with the same failure signature â the same error text, the same component, the same recovery step. Fewer than two in the last 90 days is a workaround candidate. Three or more is a fix candidate. The exact threshold matters less than having one written down; ours is three, because twice can be coincidence and three times is a pattern you can graph.
2. What does each occurrence cost?
Multiply: minutes of toil per occurrence à people involved à occurrences per year. A workaround that takes 20 minutes of on-call attention twice a month costs roughly eight engineer-hours a year. A fix estimated at three engineer-days pays back in about four and a half years at that rate â usually not worth it. Flip the frequency to weekly and the same fix pays back in three months. This carry-cost comparison is the most useful number in the conversation, and it is the one teams skip most often.
3. Does it touch customers, money, or data?
Internal failures and customer-facing failures are not the same currency. If the failure consumes error budget against an SLO â checkout availability on a Grafana dashboard, for example â or risks SLA credits, one customer-visible occurrence outweighs ten internal ones and the bar for a fix drops sharply. A recurring deadlock in a nightly batch job is an annoyance; the same deadlock on the order path during business hours is a fix, full stop.
4. Is the fix bounded?
A retry with a regression test is a bounded fix. A change that requires a schema migration on a hot Postgres table, or a rewrite of the session layer, is not. Frequent failure plus bounded fix: fix it now. Frequent failure plus unbounded fix: workaround now, with a scheduled revisit and the design problem on the engineering backlog. Rare failure plus bounded fix: fix it opportunistically the next time the file is open anyway. Rare failure plus unbounded fix: workaround, and stop feeling bad about it.
5. Is the workaround hiding the failure?
The most expensive workaround is the one that works so well nobody records it anymore. If the response has become muscle memory â no incident entry, no tally, just a quick kubectl rollout restart â you have lost the evidence you would need to revisit the decision. Keep the alert. Keep the count. A workaround with a rising tally is a fix in disguise.
Scoring: any two of {three or more occurrences, customer-facing impact, bounded fix} point to a code fix. Otherwise, document the workaround with an owner and a review date. If the team is split, write down both carry-cost estimates and check the tally again after the next two occurrences â the data settles it faster than another meeting.
What a good workaround looks like
- A runbook entry with the failure signature. Exact error text, the component, the commands, and one verification step. Keep it in a
runbooks/directory in the repo or in your docs tool; the location matters less than the link from the alert. - An alert that points to the runbook. Prometheus alert rules support a
runbook_urlannotation, and PagerDuty and Opsgenie both let you attach it to the escalation. The on-call engineer should never reconstruct the procedure from memory at 2 a.m. - An owner and a review date. Every workaround gets a named owner and a slot in the quarterly open-workarounds review. Fifteen minutes, the list on a board, promote or retire each item.
- A counter. A tally line in the runbook entry, a tag in the incident tool, or a slash command in Slack. If you cannot count it, you cannot revisit it.
What a good code fix looks like
- The failing test comes first. A regression test that reproduces the failure signature before the fix exists. If you cannot reproduce it under test, you are not ready to fix it safely.
- Remove the class, not the instance. Add the timeout and the retry with exponential backoff and jitter to every downstream HTTP call, not just the one that paged you. The smallest diff that kills the failure class beats a large diff that patches one endpoint.
- Ship behind a flag or a staged rollout if the change touches a request path, and watch the recurrence metric in Grafana for one full cycle with the alert rule still on.
- Keep the runbook entry for one quarter after the fix, marked as verification-only, then delete it.
The gray zone: failures you do not own
Some recurring failures live in code you cannot change. The test still applies, but the interpretation shifts.
Vendor-side transient errors. S3 returns 500 responses and SlowDown errors under load, and AWS’s own retry guidance says clients should retry with exponential backoff. In that case the retry logic in your code is the correct fix, not a workaround â you are implementing the contract the service documents. The same reasoning applies to GCS rate limits and any managed API that documents retryable status codes.
Upstream bugs. When a library defect produces the failure, the workaround in your code ships together with an upstream issue containing a minimal reproduction. Paste the issue link into the runbook so the workaround and the upstream fix stay connected.
Config drift. If the fix is really a Terraform change, treat it as code: same test, same review path. The meaningful line is not application code versus infrastructure â it is reviewed, tested change versus undocumented manual step.

Two worked examples
Example A: the fix
Pattern: a background worker holds a long transaction on a shared Postgres connection pool behind PgBouncer, occasionally deadlocks, and stops processing. The alert fires, on-call restarts the deployment, fifteen minutes gone. Four occurrences in six weeks, mostly in the batch window but once during business hours, blocking other jobs behind the same pool. Carry cost: roughly eight hours of toil per year plus a growing risk that the deadlock lands on the order path. The fix is bounded â a lock_timeout and statement_timeout on the transaction path plus a regression test, about two days including the staged rollout. Two fix signals, frequency and bounded scope, so it ships now.
Example B: the workaround
Pattern: a TLS certificate on a rarely used internal endpoint expires twice a year, and someone renews it by hand in about ten minutes. A real fix means moving the endpoint onto managed certificates, which touches a legacy service nobody wants to reopen â a week or more of work. Carry cost: twenty minutes per year. Impact: internal only, and the guardrail is cheap: a Prometheus blackbox_exporter probe or an AWS CloudWatch synthetics canary that pages two weeks before expiry, so the workaround is never a surprise. Decision: workaround, with the probe as the guardrail and a revisit scheduled for whenever the legacy service is retired.
Both answers are correct. The difference is that each one is written down, with the numbers that justified it.
The review that keeps the decision honest
Once a quarter, list every open workaround with its tally and owner. Promote the ones with rising counts, retire the ones for components that no longer exist, and re-run the carry-cost math on anything past its review date. This is the same discipline as blameless postmortem culture â the Google SRE Workbook’s postmortem chapter is the reference we started from â applied to the smaller recurring failures that never earn a full write-up. Pair it with your alert hygiene pass: a workaround whose alert gets muted without a ticket is a decision that was never actually made.
If you run backup and recovery drills, add one line to the drill checklist: which known workarounds did the drill exercise, and did the runbook steps still work? A recovery drill is the one time a quarter you get real evidence about whether the manual steps in your runbooks are still accurate.

FAQ
How many occurrences justify a code fix?
Our threshold is three of the same failure signature in 90 days, but the number matters less than having one written down before the debate starts. Pick a threshold, log every occurrence, and let the tally argue for you.
Is a runbook ever a permanent solution?
Rarely, and only for failures that are genuinely external and genuinely rare â vendor-side expiry events, annual certificate rotations on legacy endpoints. Everything else is a placeholder with a review date. If a runbook entry is two years old and still gets used, either the fix was never worth it or nobody ran the math.
What if the team disagrees about the decision?
Write down both carry-cost estimates in the incident review â the cost of the workaround over the next six months and the cost of the fix â then check the tally after the next two occurrences. Disagreement usually comes from different frequency guesses, and the tally settles that better than another meeting.
Should every workaround be scripted?
Script the steps that are mechanical and safe: service restarts, cache flushes, certificate renewals with a dry-run flag. Keep manual the steps that require judgment â anything touching customer data deletion or access changes. A script with a --dry-run flag and a verification step is the goal; a script that deletes data without a check is a new incident waiting for a date.
How do we count occurrences if nobody files incidents?
Fix the logging before the decision. A recurring failure that nobody records will never graduate to a fix, because the evidence does not exist. Start with a single tag in the incident tool or a tally line in the runbook entry â one month of honest counting is usually enough to make the call.
Does this test work for infrastructure, not just application code?
Yes, and the line is the same: a reviewed Terraform change with a readable plan output is a fix; a manual console edit that nobody documented is a workaround with extra risk. The test does not care where the change lives â it cares whether the change is reviewed, tested, and counted.
The decision itself is cheap. What is expensive is re-litigating it every time the failure returns. Write down the tally, the carry cost, and the review date, and the next occurrence answers the question instead of reopening it.









