Insights
Platform15 September 2026

Six services down. 21,000 cache errors. The cache was idle the whole time.

The ticket said six services were down and blamed infrastructure. The cache was healthy for the entire window. One stale image build was starving its own thread pool. Here is the measurement that settled it in four minutes.

Pattern·Silent drift below alert thresholds

Six customer environments down. Nothing shared but the image.

Cache, database and auth were per-namespace, and all healthy.

502

env 1

502

env 2

502

env 3

502

env 4

502

env 5

502

env 6

own cache
db, auth

healthy

own cache
db, auth

healthy

own cache
db, auth

healthy

own cache
db, auth

healthy

own cache
db, auth

healthy

own cache
db, auth

healthy

One stale image build

Synchronous I/O on an async path. Thread pool starves. Every outbound call times out, including the cache.

What the app reported

21,000 cache errors

What the cache reported

CPU <1%, conns flat ~80

Why they were stuck: tags changed from bare integers to semver, and the image updater silently stopped advancing anything on the old format. No failure. No alert. Six production environments drifted for months.

An automation that stops working without failing is worse than no automation.

The ticket came in already wrapped in a neat story: six services down, infrastructure. Six customer environments returning 502. Events stopped integrating into downstream records. The application had logged the same error roughly 21,000 times, and the error named the cache.

Twenty-one thousand cache errors will make most of us look at the cache first, and that is reasonable. It gives you a component name, a direction of travel and a team to page. But a good-looking error message is still only a witness statement, and this one was wrong.

Before touching anything I pulled the managed cache metrics for the exact burst window.

  • Engine CPU utilisation peaked under 1 percent.
  • Current connections sat flat at around 80, with no drop and no spike.
  • No failover, no eviction, no memory pressure, no disconnections.

The cache was idle, healthy and continuously connected for the exact window that mattered. That is the point where the ticket should change shape. If the dependency never saw the pressure, then those 21,000 cache errors are not the cache describing its own failure. They are the client telling you it could not get far enough to make the call.

If the dependency you are blaming shows no load, no errors and no disconnections during the exact window, you are not looking at an infrastructure failure. You are looking at a client that could not make the call.

The cause was one image build performing synchronous I/O on asynchronous request paths. A blocking flush on a stream, inside a request handler. The runtime is explicit about this and throws on it, but the damage is done before the exception: under load, blocking calls on async paths consume thread pool threads and do not return them.

The chain from there is mechanical.

  • A blocking call on an async path takes a thread pool thread and holds it.
  • The pool starves. Every outbound call now waits on a thread that is not coming.
  • Cache calls time out and are logged as cache errors. Twenty-one thousand of them.
  • The heartbeat endpoint times out too. nginx logs upstream prematurely closed.
  • The ingress has no healthy backend. Customers get 502.

One blocking call, 21,000 errors, the wrong name on all of them

A synchronous flush on an asynchronous request path.

Blocking flush on a stream

Called on an asynchronous request path. It blocks the thread it is running on, and that thread never goes back to the pool.

Normal traffic

3/16 held

A thread picks up a request, finishes it, and goes back to the pool.

Blocking flush on the async path

10/16 held

Every request that reaches the flush parks a thread and does not return it.

Pool starved

16/16 held

No thread is left to run any callback, including the replies already waiting to be read.

thread held, not returnedqueue depth climbsevery outbound call ages out

Everything that needs a thread now times out

Cache calls time out

21,000 cache errors logged

named in every alert

Heartbeat endpoint times out

readiness never answers

Reverse proxy gives up

upstream prematurely closed

Ingress has no healthy backend

no endpoint left to route to

Customers get 502

Six environments, one after another, from a single parked thread.

The cache, measured at the cache, same window
engine CPU peak <1%connections flat ~80no drop, no spike

It served every request it was given. The timeouts were on the calling side, where no thread was left to read the reply. The cache is not the cause. It is one of the victims, and the only one with a name in the log line.

The component named in the error was the one thing that was working.

The pattern looked broad because six customer environments failed together, but the shared thing was not the cache, the database or authentication. Each namespace had its own copy of all three, and all three were healthy. The shared thing was the image. Anomaly detection then made the suspicion concrete: one build version accounted for 100 percent of the cache errors, which moved the investigation out of infrastructure and into release state.

The more useful lesson is why those six were still on that build. Image tags had changed format, from bare integers to semantic versions, and the automated image updater had silently stopped advancing anything left on the old format. It did not fail. It did not alert. It still looked present and healthy. For months, six production customer environments sat behind automation that everybody believed was doing its job.

Two tag formats, one updater, six frozen environments

The automation did not fail. It quietly stopped seeing half the fleet.

Semantic version tags
2.153.426.12.153.481.42.154.012.2still advancing

The updater recognises the format, finds a newer build, and raises the tag. Exactly as designed.

Bare integer tags
133208215217updater stops here
months, still 217

Same updater, same registry. It reads the tag, finds nothing it considers newer, and moves on.

Why months went by

it did not failit did not alertreconciliation still reported Syncedworkloads still Running and Ready

Nothing in the pipeline turns red when an updater has simply run out of things it recognises. Present, healthy and years behind all look identical from the outside.

the comparison nobody ran: deployed tag vs newest built tag

What was actually running in production

217

env 1

217

env 2

217

env 3

217

env 4

217

env 5

217

env 6

Related, and worse

Several services also run a floating latest tag whose digest matches no numbered release. The version dimension in the observability platform then points at a label that cannot be traced back to a build, so the drift cannot be seen there either.

An automation that stops working without failing is worse than no automation.

An automation that stops working without failing is worse than no automation at all. Everyone still believes it is running, so nobody checks.

Three questions for your own estate.

  • When your application logs errors naming a dependency, does anyone check that dependency's own metrics for the same window before an infrastructure ticket gets opened?
  • If your image updater stopped advancing tags tomorrow, what would tell you, and how long would it take?
  • Can you answer which build version is producing a given error, for every service, right now?

That last one is not rhetorical. Several services in that estate run a floating latest tag whose digest matches no numbered release. That tag reaches the observability platform as the service version, which makes the version dimension meaningless for exactly the services running it. Which build is erroring was the question that cracked this incident. On those services it is unanswerable.

Six services are down described the symptom perfectly and the cause not at all. The measurement that redirected the entire investigation took four minutes to pull.

ShareLinkedIn

Get the next one in your inbox

One short, opinionated field note per fortnight on platform engineering, cloud, and making AI work in production. No spam. Unsubscribe anytime.

Senna Semakula

Senna Semakula

Founder, Atruvo

Start with a free 30-minute Platform Failure Map call.

Tell me what is happening and what you have already tried. Bring an architecture diagram, cloud bill or incident summary if you have one. We will discuss the likely risks, what to check first and whether I can help.

Have a contract role? View availability.