DEV Community

Harish
Harish

Posted on

Coding is only a part of Engineering!

How setting up a local test bench uncovered a proxy tier that had never worked, a dashboard that couldn't show it, and a secret-sync failure mode that nobody was watching.


It started with a browser that wouldn't connect

I was setting up CrawlGym — our local bench for replaying crawls against recorded HTML, so we can test hypotheses and measure regressions without hammering real sites. Everything worked
except the browser tier: every escalation to LightPanda failed to connect.

So I went to check whether production had the same problem. It didn't. It had a worse one.

Production was doing something worse

The datacenter LightPanda sidecar was logging this on every single page:

$scope=frame     $level=error $msg="navigate failed" err=CouldntResolveProxy
$scope=telemetry $level=warn  $msg="postEvents"      err=CouldntResolveProxy events=4 dropped=133
Enter fullscreen mode Exit fullscreen mode

Not some pages. Every page — including LightPanda's own telemetry, which has nothing to do with our crawler. Whatever was broken was broken for every outbound request that container made.

The numbers for proxy_browser_request_total{proxy_type="datacenter"}:

window success fail
7 days 0 67,083
90 days 0 793,810

Zero successes across the entire metric retention window. This wasn't a regression. Tier 2's browser path had never worked, not once, since the metric existed.

Meanwhile tier 1 was healthy — 250,557 successful renders in the same 7 days. So it wasn't the browser, the image, or the network. It was specific to the datacenter tier.

One secret, two consumers, one of them broken

CURLE_COULDNT_RESOLVE_PROXY means libcurl couldn't resolve the proxy hostname — a failure before any socket opens. But the same pod's Go process was resolving that exact host fine and getting HTTP 200s through it. Same network namespace, same /etc/resolv.conf.

The two processes weren't being handed the same string.

PROXY_DATACENTER_URL in Vault ended with a trailing \n. One byte, and it broke exactly one of the two consumers, because they read the variable by completely different routes:

consumer path the value travels result
Go crawler env → envsubst into config.yaml text → YAML parse newline absorbed as end-of-scalar → clean URL, proxy works
LightPanda k8s $(VAR)argv, no YAML anywhere newline preserved → host proxy.example.net:8080\n → unresolvable

That YAML absorption is the whole reason this survived for months. After substitution the config line becomes a clean scalar followed by a harmless comment line. The Go side parses fine, logs datacenter_http:true at startup, and pushes real traffic through the proxy. No error, no warning, nothing to find.

I reproduced it against the exact production image to confirm which input shape produced which error:

--http-proxy value error
valid host, with or without credentials CouldntConnect
scheme-less with credentials CouldntConnect
trailing newline CouldntResolveProxy
nonexistent host CouldntResolveProxy
literal unexpanded $(VAR) CouldntResolveProxy

LightPanda resolves proxy hostnames perfectly well. It just can't resolve one with a line break glued to the end.

The cost: every page needing a render that tier 1 couldn't get escalated to datacenter, failed 100% of the time, and fell through to the web unlocker at 72× the cost weight. In one week,
112,228 forced escalations to the expensive tier — all downstream of a tier that structurally could not succeed.

Three proxy tiers. Tier 1 direct is healthy at 250,557 renders. Tier 2 datacenter is boxed in red dashes: 0 successes against 67,083 failures, structurally impossible. Everything falls through to tier 3, the web unlocker, at 72x the cost weight — 112,228 forced escalations in one week.

Why the dashboard showed nothing

We have a Proxy Observability dashboard with a panel literally titled "Tier 2 (Datacenter) requests: success vs failure". It looked fine.

It queried proxy_request_total — the HTTP fast path. Only that. The browser path, proxy_browser_request_total, wasn't on the panel at all.

So a tier where the HTTP half worked and the browser half was 100% dead rendered as a healthy green line. The panel wasn't wrong, it was incomplete — and incomplete in exactly the dimension where the failure lived. Its description even claimed the tier "has recorded no direct successes historically", which was stale and pointed attention away from the real gap.

Two panels side by side. The one we had plots only the HTTP path: a single healthy line near 250k. The one we needed plots both: the same healthy HTTP line, plus a browser series pinned flat at zero for the full 90 days.

The fix was two queries instead of one, with legends that can't blend:

A:  sum by (failure_category) (rate(proxy_request_total{...proxy_type="datacenter"}[$__rate_interval]))*3600
    legend: HTTP · {{failure_category}}

B:  sum by (outcome) (rate(proxy_browser_request_total{...proxy_type="datacenter"}[$__rate_interval]))*3600
    legend: Browser · {{outcome}}
Enter fullscreen mode Exit fullscreen mode

A tier with two independent failure paths needs two series. Aggregating them into one number is how you get a dashboard that is technically accurate and operationally useless.

Then dev refused to accept the fix

I patched the newline out of Vault in dev, restarted, and nothing changed. The pods still came up with the old value.

Two things were going on.

The boring one: environment variables from secretKeyRef are resolved at container start.
Updating a Secret never changes a running pod's environment — new pods are always required.

The second was not boring. The dev Secret wasn't updating at all. The sync had been dead for seven weeks:

error processing spec.data[2] (key: .../my-app-llm-api), err: Secret does not exist
Ready=False   refreshTime=2026-07-15T06:48:31Z
Enter fullscreen mode Exit fullscreen mode

The External Secrets Operator resolves every key in an ExternalSecret atomically. If one key fails, it writes nothing — so one missing key also froze PROXY_DATACENTER_URL, the database credentials, and everything else in the same object. Seven services, all stuck on a copy of their secrets from 15 July.

And here's the part that should worry you: nothing looked broken. ESO doesn't delete the Secret when it fails, it just stops updating it. Pods kept booting from the last-good copy.
One of them was still making successful API calls that morning, on a key synced seven weeks earlier. The only signal anywhere was a Ready=False on an object nobody looks at.

The fix was to create the one missing Vault path — present in prod, never created in dev — populated with the value already working, so restoring the sync was a no-op for that key rather than a credential rotation. All seven went Ready=False → True within two refresh cycles.

Restoring the sync then did something nice: it pushed the corrected proxy URL into the five secrets I'd never hand-patched, which were all still carrying the newline. And it validated the
blind Vault write independently — ESO fetched from Vault and produced byte-for-byte what I'd patched by hand.

The failure mode is bigger than us

Once I understood the shape of it, I audited every ExternalSecret in the namespace.

Dev: 15 of 200 failing. Ten were ours. The rest belonged to other teams:

  • Three referenced a Vault path prefix that has never existed in that cluster — secret/data/my-company.org/... instead of the .../ns/<namespace>/<app> layout everything else uses. Their refreshTime is None. They have never synced, ever.
  • One had the right path but the wrong property name inside it.
  • Four wildcard TLS certificates were healthy until 2026-08-17, then the Vault entry vanished.

Prod: far better — 195 secrets, only 2 failing. But those two are instructive. They last synced on 2026-03-05. Six months ago. The consuming deployment is running 1/1 right now; its
pod started 2026-08-19, five months after the sync broke, and came up fine on the retained Secret.

So a production service is quietly running on a credential that can no longer be rotated.
Write a new key to Vault and it will never reach the pod. The eventual failure will look like vendor auth errors, with no obvious connection to a secret change made months earlier.

That's the real defect. Not any individual missing path — those are ordinary mistakes. The defect is that a broken secret sync is invisible by design: retention means the app keeps
working, so the failure is deferred to whenever someone next needs the value to actually change.
Which is usually a rotation, an incident, or a migration — the worst possible moments to discover your secrets have been frozen since March.

I raised this with the cloud team. They're now looking at surfacing sync failures during ArgoCD sync, so a Ready=False ExternalSecret fails loudly at deploy time instead of lurking. I handed
over the other teams' broken secrets along with it.

What I'd take from this

A single byte can be invisible on one path and fatal on another. The same value went through YAML on one side and argv on the other. Ask what transformations sit between a config value
and each of its consumers — they're rarely the same.

Verification methods can launder the bug. Shell $( ) strips trailing newlines. tr turns them into line breaks. Both "confirmed" the URL was well-formed. Use xxd.

A panel that shows one of two failure paths is worse than no panel, because it manufactures confidence. If a tier can fail two independent ways, plot two series.

Retention makes failures polite, and therefore dangerous. ESO keeping the last-good Secret is the right default for availability and the worst possible default for observability. If a system's failure mode is "everything keeps working, just frozen", nothing short of an explicit check will catch it.

Absence of a metric is not evidence of absence. Two signals nearly derailed the diagnosis:
ESO's syncedResourceVersion didn't change after a write that definitely landed, and a brand-new pod had no Prometheus series at all — not zero-valued ones — because of scrape discovery lag
and client_golang only creating a labelled series on first increment. When in doubt, read the pod's own logs.

The part that has nothing to do with newlines

Here's what actually bothers me about this bug.

We use LLMs to generate code now. That's fine — it's a tool, and it lets us implement things faster than we could before. But it only compresses one part of the job. Every other part of
engineering still exists.

For starters: we still have to make sure the damn thing we built works.

Testing methodology and observability matter more now, not less, precisely because we pay less attention to the implementation. If you didn't write the code line by line, you don't carry a
mental model of where it's fragile — so the only thing standing between you and a silent failure is a strict test that fails loudly and an alert that fires when reality diverges from what you
assumed.

This bug is what that gap looks like in production. A tier shipped that had never worked once. 793,810 failures over 90 days. A dashboard panel that couldn't show the failing half. Seven
services frozen on seven-week-old secrets, and the only evidence was a Ready=False field on an object nobody reads. Nothing here was hard to detect. It just wasn't being checked.

Impact assessment, testing, monitoring after deployment — none of that vanished with AI. You can use AI to help with all of it: have it write the test you'd have skipped, generate the alert
rule, review the diff for what you forgot to instrument. The failure here wasn't AI. It was skipping the cadence.

Writing the implementation got faster — minutes, not days. Impact assessment, testing, observability and post-deploy checks did not shrink at all. Four things went unchecked in this bug: a tier that never worked once, a panel plotting one of two failure paths, seven services frozen on seven-week-old secrets, and verification that laundered the bug. None of it was hard to detect — it just wasn't being checked.

And I'll be honest about the uncomfortable part: because we aren't writing the code, we care less. When you can generate a working implementation in a minute, it becomes very easy to stop
at "it runs" and never do the complete thing. That pull is real, and I feel it too.

Doing the complete thing anyway is the job. That's what being a responsible engineer means, and it's the part no tool is going to do for you unless you ask it to.

Top comments (0)