Open any production Kubernetes cluster that runs a service mesh and count the containers. Pick a pod with a small Go service in it, 40 MB of resident memory, doing real work. Next to it sits an Envoy proxy that is, in many clusters, using more memory than the service it protects. Multiply by 800 pods. That proxy fleet is a line item, and somebody on the platform team has been asked about it in a budget review.
The sidecar pattern is the reason. It is one of the most useful ideas in distributed systems and one of the easiest to overpay for, and the interesting part of the last two years is that the platform people have started separating the two.
My claim: the sidecar is a pattern about lifecycle and isolation, not about proxies. If you keep that definition, you will use it well in places a mesh never touches. If you equate "sidecar" with "Envoy next to every pod", you will end up with the bill and miss the idea.
What the pattern actually says
The Azure Architecture Center states it in one sentence: deploy a cohesive set of tasks alongside the primary application in a separate process or container, to get a consistent interface for platform services across languages. The sidecar "connects to the application without being part of it", and each application instance gets its own sidecar instance that shares its life cycle.
Notice the two constraints that make it a sidecar and not just "another service". It is deployed per instance, and it lives and dies with the instance. A log shipper that runs once per cluster is a daemon. A rate limiter that every pod calls over the network is a service. A log shipper that runs next to every pod, reads the same volume, and is killed when the pod is killed is a sidecar.
The reasons to pay for that arrangement are real. The Azure page lists them: language independence (one implementation across a Java service, a Python service and a Go service), access to the same resources as the primary app, low latency because the two are neighbors, and the ability to extend an application you cannot or do not want to modify. The last one is the whole story of the service mesh. Nobody wanted to teach 40 services in five languages to do mTLS, retries and tracing the same way.
The same page is just as direct about the cost. Sidecars add overhead, especially latency. In small applications the resource cost of a sidecar per instance can outweigh the isolation benefit. If a component has to scale differently from the app, it should be a separate service. And if your platform already provides the capability natively, a sidecar is complexity you did not need.
Hold on to that last sentence. It is where this essay ends up.
The sidecar you should write yourself
Before the mesh, the small version, because it is the one you will actually build. Here is a log shipper next to an application, expressed the way Kubernetes now wants you to express it:
apiVersion: v1
kind: Pod
metadata:
name: orders
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:3.1
restartPolicy: Always # this line makes it a native sidecar
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: orders
image: registry.example.com/orders:2.14.0
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
The app writes files to a shared emptyDir. The sidecar tails them and ships them wherever your logging backend lives. The app has no logging client, no backend credentials, and no idea the backend changed last quarter. That is the pattern working as advertised.
The interesting line is restartPolicy: Always on an init container. For years a sidecar in Kubernetes was just a second entry in containers, which meant the platform had no idea it was special. Native sidecar containers arrived in Kubernetes v1.28 and, per the official docs, are stable since v1.33. The behavior I rely on, from the design: sidecars start in the order listed, before the regular containers, and a startup probe on a sidecar gates the containers after it. On shutdown they stop after the main containers, in reverse order. And a sidecar does not keep a Job from completing.
That last property fixes the oldest sidecar bug in the book. Run a Job with a proxy in a regular second container and the Job never finishes, because the proxy never exits. Teams wrote kill -TERM hacks, shared-volume "done" files, and wrapper scripts to get around it. With native sidecars, the Job finishes when the main container does and the platform terminates the sidecar. The other classic failure, the app starting before its proxy is ready and failing its first outbound calls, goes away for the same reason: ordering is now a platform guarantee, not a race.
If you take one thing from this section: if you run sidecars on a cluster at v1.33 or later, move them to initContainers with restartPolicy: Always. It is a small diff and it deletes a category of incident.
How a mesh turns the pattern into a fleet
A service mesh is the sidecar pattern with an operator attached. In Istio's sidecar mode you label a namespace and a mutating admission webhook injects an istio-proxy container into every new pod in it:
kubectl label namespace payments istio-injection=enabled --overwrite
kubectl rollout restart deployment -n payments
Two details in those two lines explain most of the pain. First, injection happens at pod creation, so nothing changes for existing pods until you restart them. Second, the proxy intercepts traffic by rewriting the pod's network rules, which is why an init step has to run before your app does. Your workload does not call the proxy. The kernel quietly sends its traffic there.
That design is what makes the pattern magic and what makes it expensive. The proxy is Envoy, a full L7 engine, and you now run one per pod regardless of whether that pod needs L7 features. Istio's own performance page gives the baseline for Istio 1.24 at 1,000 HTTP requests per second with mTLS on, 1 KB payloads and two proxy worker threads: a sidecar uses about 0.20 vCPU and about 60 MB of memory. Those numbers come from a specific benchmark on a bare-metal cluster, and the page itself says other hardware will produce different values. Treat them as a unit of account, not a forecast.
Run the arithmetic anyway, because it is the arithmetic the budget review does. At 0.20 vCPU and 60 MB each, 800 sidecars cost roughly 160 vCPU and 48 GB of memory, reserved for proxies that mostly do mTLS and emit metrics. Your services may be far quieter than the benchmark, in which case the CPU number falls and the memory number does not. A proxy has a floor.
Resource cost is only the first of three problems. The Istio team's own list, from the ambient GA announcement, is blunt: sidecars could break applications once added, each workload's proxy needed substantial CPU and memory, and every Istio release required restarting application pods. The third one is the operational killer. When the mesh is injected into the pod, the mesh upgrade is an application rollout. The platform team cannot patch a proxy CVE without asking every product team to redeploy.
What replaced the expensive part
Istio's answer is ambient mode, which the project declared generally available in v1.24 in November 2024. It splits the sidecar's two jobs apart.
The base layer is ztunnel, a per-node proxy, written in Rust and deliberately limited to L3 and L4. Per the ambient overview, it provides the secure overlay: mTLS, workload authentication, L4 authorization and L4 telemetry. It does not terminate workload HTTP traffic or parse HTTP headers. Traffic between nodes rides a tunneling protocol called HBONE, which is built on HTTP CONNECT.
The optional layer is the waypoint proxy: Envoy, the same engine the sidecars use, but deployed outside the application pods, installed and scaled on its own, and only where a namespace actually needs L7 behavior like HTTP routing or L7 authorization. The overview is explicit that waypoints are heavier than the ztunnel overlay alone, which is the point. You pay for L7 where you use L7.
Enrolling a namespace is a label, with no restart:
kubectl label namespace payments istio.io/dataplane-mode=ambient
# only if payments needs HTTP-level policy or routing:
istioctl waypoint apply -n payments --enroll-namespace
The same performance page puts the proxies side by side under the identical benchmark: ztunnel at about 0.06 vCPU and 12 MB, a waypoint at about 0.25 vCPU and 60 MB, a sidecar at 0.20 vCPU and 60 MB. Read those carefully, because the lazy conclusion is wrong. A waypoint is not cheaper than a sidecar per instance. It is cheaper because you run a few of them per namespace instead of one per pod, and a ztunnel is one small process per node instead of one per pod. The announcement says resource savings in some use cases can exceed 90 percent, and one adopter reported cutting about 45 percent of their running containers after removing sidecars and SPIRE agent DaemonSets. Both are claims about specific workloads, and your ratio depends on your pod density and how many namespaces truly need L7.
Notice what happened to the pattern. The capability (mTLS, policy, telemetry for every workload, no application changes) survived. The deployment shape (a proxy inside every pod's lifecycle) was dropped. That is the Azure page's final drawback in action: if the platform can provide it natively, a sidecar is complexity you did not need.
The yes, but
The strongest objection is that the sidecar's tight coupling is a feature. A per-pod proxy has a failure domain of exactly one pod. If it crashes, one instance has a bad day. If a node-level ztunnel misbehaves, every meshed pod on that node is affected. Per-pod proxies also give you per-pod resource limits, per-pod configuration and a clean answer to "which team owns this proxy". The Azure page lists fine-grained resource limits as a legitimate reason to choose a sidecar, and for noisy-neighbor-sensitive or strictly isolated workloads that still holds.
Sidecars are also not going away: the Istio announcement says plainly that they remain fully supported, first-class options, and the project's own roadmap at the time listed sidecar and ambient interoperability and multi-cluster support as upcoming work. If you need features that sit on the unfinished side of that list, check the current docs before you plan a migration, because I am describing the state of the documentation, not your cluster.
And there is a point the mesh debate hides: most sidecars are not meshes. Your log shipper, your secret refresher, your Dapr runtime, your OpenTelemetry Collector next to the app, your database proxy that handles IAM auth. Those have no node-level equivalent that is better, and they benefit directly from native sidecar ordering. Nothing in ambient mode argues against them.
What to do on Monday
Start with an inventory, not an opinion. For each sidecar in your cluster, write down what job it does and ask the Azure question: could the platform do this natively? Per-pod log shipping can often become a node-level DaemonSet. A per-pod mTLS proxy might become ztunnel. A sidecar that exists because one team wanted a different retry policy probably should be a library.
Then fix the cheap thing first. On Kubernetes v1.33 or later, convert every hand-rolled sidecar to a native one. You get ordered startup, ordered shutdown and Jobs that finish, in exchange for one new field.
Finally, before you adopt or leave a mesh, measure your own proxy cost with your own traffic. Take the Istio numbers above as the shape of the problem, multiply by your pod count, and compare it against what the mesh gives you that a library and a good ingress would not. If the answer is "mTLS everywhere and uniform telemetry", ambient mode is worth a staging cluster. If the answer is "we turned it on because everyone does", that is the actual finding.

