The lie in a green checkmark
There’s a specific kind of outage that only happens on Tuesdays at 2 PM. Nothing crashed. No alerts fired. The deploy pipeline turned green, Slack posted the celebratory rocket emoji, and everyone moved on. Three hours later a support ticket lands: “the export button does nothing.” You check the logs. The service is up. Health checks pass. But a background worker has been silently dropping jobs since the last release, because a config key was renamed and the new binary is falling back to a default that nobody remembered writing.
The line deploy succeeded was, technically, true. It was also the most expensive line in the entire log stream.
What the checkmark actually means
Most deploy systems answer a very narrow question: did the new artifact start running without immediately dying? That’s a long way from “the system works.” A container can boot, respond to /health, register with the load balancer, and still be catastrophically broken in ways that only surface under real traffic, real data, or the specific hour of the day when your batch jobs run.
There’s a pattern I keep seeing in engineering orgs: the more mature the CI/CD pipeline looks, the more confident engineers become in signals that were never designed to carry that weight. A liveness probe tells you a process answered a TCP call. It tells you nothing about whether the answers are correct, whether the right code path is being reached, or whether the feature a customer paid for still functions. “No errors in the last 60 seconds” is mostly a statement about your logging.
The green checkmark is the moment where writing ends and running begins, and most pipelines treat it as the finish line.
What silent failure actually looks like
The failures that hurt most in production tend to share a shape:
- Partial. One code path is broken, the other ninety-nine are fine. Aggregate error rates barely move.
- Delayed. The bug manifests on a cron job, or a webhook retry that happens hours later, or the first customer who logs in from a different timezone.
- Semantic. The system returns a 200 with the wrong data. Nothing is technically an error.
- Configuration-shaped. The code is fine. The env var, feature flag, secret, or IAM policy changed underneath it.
LLM features amplify every one of these. A prompt template tweak can degrade output quality without touching latency or error rates. When a provider rolls a model version, token distributions shift enough to break downstream parsers, and the only symptom is a JSON schema validator quietly falling back to a retry path you added months ago and forgot about. The pipeline still says deploy succeeded. The feature still “works.” The cost per request just tripled.
Designing for loud failure
The fix isn’t more dashboards. Treat the deploy itself as a hypothesis that has to be actively disproved before it’s accepted. A few principles worth internalizing:
Every deploy is a controlled experiment. The artifact is the treatment. Production traffic is the trial. You need a pre-registered outcome that either confirms or rejects the deploy. Define, before you ship, what “actually working” looks like for your specific system. For an e-commerce checkout, that might be completion rate holding steady against the previous hour. For an LLM feature, it’s often tool-call success rate and average tokens per request staying inside a band you’ve established from a week of baseline data. The specific thresholds don’t matter as much as the fact that somebody wrote them down and the pipeline can read them.
Ship behind a promotion gate, not a deploy gate. The moment new code starts running is not the moment it should own traffic. Progressive delivery, canary rollouts, shadow deploys, ring-based rollouts, all buy you an observation window where silent failures become loud. If your platform can’t do this yet, fix that before you put an LLM anywhere near a production path.
Verify the contract, not the process. Post-deploy synthetic checks should exercise real user journeys with actual assertions on output. For LLM paths, that means running a small eval suite against the deployed version. A handful of prompts with expected structural properties is enough, and if the outputs drift, the promotion fails. It’s cheap. Almost nobody does it.
Diff the runtime, not just the code. Most silent failures come from environment drift. A config value flipped, a secret rotated, some dependency pinned to a version somebody forgot about. A useful discipline is snapshotting the effective configuration at deploy time and diffing it against the last known-good. If a value changed and nobody put it in the release notes, that’s worth waking somebody up about.
The LLM-specific tax
If you’re running an AI feature in production, silent failure has a second cost: money. A retry loop that was previously rare can suddenly fire on a large fraction of requests after a prompt change, and your provider bill will notice before your monitoring does. This is one of the reasons build-versus-buy decisions on AI infrastructure are harder than they look. Hosted providers hide the failure modes that would be screamingly obvious if you were running the model yourself, and self-hosting exposes you to failure modes the provider was quietly absorbing on your behalf.
The practical move is instrumenting cost per successful outcome rather than cost per request. If a deploy doubles your retry rate, you want to see a step change in cost-per-conversion, not a subtle uptick in a token counter that nobody watches.
What to actually change on Monday
If your pipeline currently ends with a green check and a Slack ping, three concrete additions tend to pay for themselves quickly:
- A post-deploy verification step that runs real user-journey assertions, including at least one that exercises any LLM or third-party dependency. It marks the deploy as
unverifiedrather thansucceededuntil it passes. - An automatic configuration diff published as part of the release notes, so environment drift stops being invisible.
- A boring, one-button rollback. If rolling back requires a group meeting, the team won’t do it fast enough, and silent failures will live longer than they should.
None of this is exotic. Until something actively confirms it, the most honest log line your system can produce isn’t deploy succeeded, it’s deploy started, verification pending. Change the framing on Monday and see how much argument you get from the people who’ve been on-call the longest. In my experience they’ll be the first to agree.
Further reading
- Canarying Releases, Google SRE Workbook chapter on structured canary analysis
- Release Engineering, Google SRE Book on the discipline behind safe deploys
- Observability vs. Monitoring, on why “no errors” is mostly a statement about your logging