> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# The Sidecar That Bound the App's Port Before the App Could
- URL: https://debugly.dev/sidecar-ate-app-port/
- Published: 2026-10-09T13:37:00.000Z
- Updated: 2026-10-17T05:56:28.000Z
- Description: The pod crash looped with EADDRINUSE on its own port at boot, because the sidecar started faster and bound the port first, and the app, losing the race it…
- Author: Rohit Bhadani
- Tags: Error Autopsy, Kubernetes, Networking

The pod crash looped with the least ambiguous error in networking, EADDRINUSE, the port is already in use, and the port it could not bind was its own, the one the app had owned for a year, which made the error read as impossible, because nothing else in the cluster was supposed to want that port. The something that wanted it was in the same pod, a sidecar added the previous sprint, which started in two seconds and bound the port, while the app, starting in nine, arrived to find its address taken by its own companion.

This is the autopsy of a startup race inside a pod, and of the assumption that broke: that containers in a pod start in an order, or that the port belongs to whoever the docs say it belongs to, when in fact the port belongs to whoever binds it first.

This was Kubernetes 1.32 with a two container pod, and the race is deterministic in outcome and nondeterministic in trigger, which is the worst combination, because it fails on the slow boots, which are the cold starts, which are the deploys.

## The quick version

Containers in a pod share the network namespace, and therefore the port table, and Kubernetes starts them without an ordering guarantee unless you build one. The sidecar's job was to proxy telemetry, and it bound the app's port to intercept, by design, but the design assumed the app would bind first and hand over, or that the sidecar would wait, and neither assumption was enforced, so on boots where the sidecar won the race, it held the port, and the app died with EADDRINUSE, and restarted, and lost again, because the sidecar, once up, stays up, and the app loops against a port that will never be free, which is the crash loop.

The error names the port, and the port names the owner, and the owner, per the pod's own spec, is the sidecar, so the diagnosis is in the manifest, which is the last place the on call looked, because the manifest is assumed correct, and the assumption is the bug.

## Why it only failed on cold starts

The race is won by the faster starter, and on warm nodes with cached images the app sometimes won, binding before the sidecar, which then failed to bind and, being the tolerant one, retried and attached after, which is the working order, arrived by luck. On cold starts, the sidecar's smaller image pulled faster and won, and the order inverted, and the app died. So the incident correlated with deploys and scale ups, the cold start events, which is the signature of any startup race, per [the failover that promoted a replica hours behind](https://debugly.dev/pg-failover-replica-hours-behind/), same mechanism, different costume.

## The causes, ranked

### 1\. An unenforced startup ordering

The primary defect. The design required the app to bind first, or the sidecar to wait, and neither was enforced, so the requirement lived in the authors' heads and the failure lived in the scheduler's timing. Ordering that is a requirement must be a mechanism.

### 2\. A sidecar that binds eagerly and holds

The sidecar bound the port at its own start and held it on bind failure of the app, rather than probing and yielding, so the sidecar's eagerness turned a race into a lockout, and its own resilience, restart on failure, was absent, so it never gave the port back.

### 3\. No readiness coupling

The app had no dependency on the sidecar's state and vice versa, so neither waited for the other, and the pod's readiness considered the sidecar ready the moment it bound, which is the moment the app became unable to start, so the pod reported ready while containing a crash looping app, which is the health check blindness from [readiness, liveness and the probe that restarted a healthy pod](https://debugly.dev/k8s-probe-restarted-healthy-pod/), inverted: ready on the strength of the component that caused the failure.

## The fix

The immediate fix restarted the pods until a warm boot won the race, which is a confession, and the durable fixes were three.

First, the ordering became a mechanism: the app container got a startup probe gating readiness, and the sidecar was changed to bind only after the app's port answers, probing first and yielding on conflict, so the sidecar's bind is conditional on the app's absence, which removes the race by making the sidecar the waiter.

Second, the sidecar's bind became non fatal and retrying, so a lost race degrades the sidecar, not the app, and the sidecar retries with backoff until the port is shared as designed, which is the blast radius moved to the less critical container.

Third, the pod's readiness now requires the app's startup probe, so a pod cannot report ready while its app is crash looping, which fixes the monitoring half, because the ready lie is what let the loop run unnoticed.

## What I would do differently

I would have read the manifest as a concurrent program, because a pod is one, and the manifest's containers are threads with a shared port table, and concurrent programs get reviewed for races, and the review question is, for each shared resource, who binds first and what enforces it, and the answer, nothing, was the incident.

I would also have tested the cold start, because the race lives on the cold path, and the warm path's success is the mask, and the deploy is the cold start, so the test that never cold starts never sees the bug, which is the reproduction matrix lesson again, in scheduling form.

## What I now do

Containers in a pod share the port table and start without ordering, so a port is owned by whoever binds first, and a design that assumes an order must enforce it with probes and conditional binds, or the slow starter loses its own address to its companion and loops against a port that never frees.

The EADDRINUSE was not a conflict with a stranger. It was a conflict with the pod's own design, executed at the scheduler's timing, and the manifest, read as a concurrent program, contains the whole bug, which is the property that makes startup races the most preventable incidents there are, and among the least prevented.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)