The main argument against service mesh has stayed the same for years: to get mTLS and observability, you have to stuff an Envoy proxy into every pod. That’s an extra process, extra memory, an extra restart on every sidecar injector rollout, and a separate headache with init containers and startup ordering. Istio’s ambient mode removes the sidecar from that equation: mTLS and part of the telemetry now come almost free at the node level, and heavy L7 logic only turns on where it’s actually needed.
Table of contents
Open Table of contents
- Why a service mesh at all
- What’s wrong with sidecars
- Ambient architecture: ztunnel and waypoint
- mTLS between pods without restarting applications
- Rolling it out gradually: from L4 to everything else
- Comparison with Cilium/eBPF-based mesh
- What you need to try ambient
- How to verify it works
- Bottom line
Why a service mesh at all
Service mesh solves a narrow but painful set of problems: encrypting traffic between services without touching application code, a single place for retries and timeouts, and observability at the “who talked to whom, for how long” level without instrumenting every service by hand. Istio became the de-facto Kubernetes standard precisely because it solved all of that declaratively, through CRDs, instead of libraries you’d have to drag into every service in every language.
But for years all of that functionality was bought at the same fixed price — a sidecar in every pod, regardless of whether a given service actually needed any of the advanced capabilities. Ambient is Istio’s attempt to decouple price from functionality, so you pay only for what you actually use.
What’s wrong with sidecars
Classic Istio works through sidecar injection: a webhook at pod-creation time adds another container to the application container — Envoy. From then on, all of the pod’s traffic gets redirected through iptables into that Envoy, which handles mTLS, retries, metrics, and routing.
The model works, but at a real cost:
- Memory and CPU per pod. Envoy isn’t a free process: a typical sidecar eats anywhere from tens to hundreds of megabytes, even for a pod that barely sees traffic. On a cluster with thousands of pods, that’s a noticeable line item just for the mesh layer.
- App restarts on every sidecar upgrade. A new Envoy version means a new pod, because the sidecar shares a lifecycle with the application. Upgrading the mesh becomes upgrading the entire fleet of pods.
- Injection complexity. The webhook has to fire before the app container starts, and container startup order plus
holdApplicationUntilProxyStartshave caused more than one production incident of “the app started before the proxy and got connection refused.” - Everyone gets the proxy, whether they need it or not. A simple service that only needs mTLS between pods gets the same heavyweight L7 proxy as a service with complex routing — the overhead is paid equally by everyone.
Ambient architecture: ztunnel and waypoint
Ambient splits what used to be a single Envoy sidecar into two independent layers that turn on separately.
ztunnel (L4) is a small proxy written in Rust, one instance per node (a DaemonSet), not per pod. It takes over the most frequent, cheapest-to-run work from the sidecar: establishing an mTLS tunnel between pods, collecting basic L4 metrics (who talks to whom, how many bytes, connection status), and enforcing L4 authorization policies (allow/deny by identity). ztunnel doesn’t parse HTTP, doesn’t retry, doesn’t look at headers — which is exactly why it stays small and cheap.
waypoint (L7) is a full Envoy proxy, but no longer tied to every pod. It’s deployed per service or per namespace, only when someone needs L7 functionality: header-based routing, retries, timeouts, weighted canary rollouts, AuthorizationPolicy scoped to an HTTP method or path. If none of that is needed, no waypoint gets deployed, and traffic flows directly through the sender’s and receiver’s ztunnels.
The key difference from the sidecar model: L4 capabilities (mTLS, basic metrics, identity) are free for everyone, while L7 capabilities (routing, retries, fine-grained HTTP policies) are only for the services that explicitly asked for them. What used to be a binary choice — all of Istio or none of it — is now a two-step scale.
mTLS between pods without restarting applications
The most tangible difference in practice: mTLS in ambient shows up without a single pod restart. Since ztunnel lives outside the application pod and intercepts traffic at the node level through a CNI plugin, enabling mesh for a namespace comes down to ztunnel starting to see that namespace’s traffic — the pods themselves are never touched.
That fundamentally changes the operational model of adoption: in sidecar mode, enabling mesh on an existing namespace meant a rolling restart of every deployment (so the webhook had time to inject the sidecar). In ambient, it’s a namespace label that gets picked up on the fly.
The redirect mechanics differ too. Sidecar redirects a pod’s traffic to itself via iptables rules generated by an init container at pod startup. Ambient uses Istio’s CNI plugin, which at the node level wraps traffic from labeled-namespace pods into the local ztunnel through an HBONE tunnel (HTTP-Based Overlay Network Encapsulation) — a protocol built on top of mTLS specifically for ambient. For the application, the difference is invisible: it sends and receives ordinary TCP connections just as before, except now those connections always route through ztunnel and are always encrypted.
Rolling it out gradually: from L4 to everything else
Ambient was designed as a model with explicit, reversible steps, not an all-or-nothing switch:
- L4 across the whole namespace. One label, and every pod in the namespace gets mTLS, identity, and basic metrics via ztunnel. Applications keep running unaffected.
- A waypoint wherever L7 is needed. As soon as a specific service needs header-based routing, weighted canary releases, or an
AuthorizationPolicyscoped to an HTTP path, a waypoint gets deployed for it. The rest of the services in the same namespace keep running through a single ztunnel, with no L7 overhead. - Turning it off is as simple as turning it on. Removing the label or deleting a waypoint isn’t a rolling restart — it’s a reversible config-level operation.
In practice, this means migrating from “no mesh at all” to “mesh with L7 on the critical services” no longer requires one cluster-wide decision — you can move service by service, judging where L7 actually pays off.
Comparison with Cilium/eBPF-based mesh
Ambient isn’t the only answer to “sidecars are expensive.” Cilium Service Mesh and similar eBPF-based approaches attack the same problem from a different angle: instead of a per-node proxy process, they use in-kernel eBPF programs for L3/L4 routing and policy. Both approaches share the same idea — move cheap, frequent work out of a userspace proxy — but the mechanics and trade-offs differ.
| Sidecar (Istio classic) | Istio ambient | Cilium (eBPF mesh) | |
|---|---|---|---|
| L4 load balancing and policy | Envoy sidecar in every pod | ztunnel, one per node | eBPF programs in-kernel, no userspace L4 proxy |
| mTLS | sidecar, per pod | ztunnel, per node | usually via a separate L7 proxy (e.g. a waypoint) |
| L7 (HTTP routing, retries) | sidecar, always present | waypoint, on demand | separate L7 proxy when needed |
| Overhead on a pod with no L7 needs | high (full Envoy) | minimal (only node-level ztunnel) | minimal (in-kernel eBPF) |
| Upgrade without app restart | no | yes (ztunnel outside the pod) | yes (eBPF isn’t in the pod) |
| Maturity in 2026 | stable for years | GA since 1.24, multi-cluster alpha since 1.27 | mature for L3/L4, L7 side is younger |
| Policy/CRD ecosystem | widest (Istio CRDs) | same CRDs as classic Istio | its own model (CiliumNetworkPolicy and relatives) |
The practical takeaway: if you’re already using Istio CRDs (VirtualService, AuthorizationPolicy) and want to keep that ecosystem, ambient gives you the same API at lower overhead. If you’re choosing a mesh from scratch and your CNI is already Cilium, it’s worth looking at its built-in mesh instead of layering a second component on top of an eBPF data plane you already run.
There’s a less obvious factor too — your team’s operational maturity. eBPF programs run in the host kernel, which means newer kernels and careful node-upgrade discipline: a bug in eBPF code can potentially take down a whole node, not just a pod. ztunnel is an ordinary userspace process that crashes and restarts like any other pod, and you can debug it with familiar tools (logs, kubectl exec, profilers). For teams that already know how to run Istio classic, moving to ambient is a change of install profile, not a change of tooling or mental model.
What you need to try ambient
The minimum for testing on an existing cluster: Kubernetes 1.28+, a CNI compatible with the ambient redirect (Istio installs its own CNI plugin), and a current istioctl.
# install the control plane with the ambient profile
istioctl install --set profile=ambient --skip-confirmation
# confirm ztunnel and istio-cni came up as a DaemonSet on every node
kubectl get pods -n istio-system -l app=ztunnel -o wide
kubectl get pods -n istio-system -l k8s-app=istio-cni-node -o wide
Enable ambient on a namespace — applications aren’t touched:
kubectl label namespace shop istio.io/dataplane-mode=ambient
Add a waypoint for a service that needs L7 (say, header-based canary routing):
istioctl waypoint apply --namespace shop --for service --name checkout
An L7 policy scoped to that service — only it now routes through the waypoint, the rest of the namespace keeps using a single ztunnel:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: checkout-only-internal
namespace: shop
spec:
targetRef:
kind: Service
name: checkout
action: ALLOW
rules:
- from:
- source:
namespaces: ["shop", "gateway"]
to:
- operation:
methods: ["GET", "POST"]
How to verify it works
Check that traffic between pods in the namespace is actually mTLS-encrypted, without going into a single pod:
# check ztunnel's connection status for the namespace
istioctl ztunnel-config connections <ztunnel-pod-name> -n istio-system
# expect the connection protocol to be HBONE (ambient's mTLS tunnel),
# not plain TCP
A second quick check is ztunnel’s own logs — confirm it sees traffic from the namespace you labeled:
kubectl logs -n istio-system -l app=ztunnel --tail=50 | grep shop
If a waypoint is deployed, confirm traffic to the service actually routes through it rather than directly — istioctl proxy-config for the waypoint pod should show routes for checkout.
A useful sanity check before enabling ambient in production: compare mesh-layer memory usage before and after on a staging copy of a real namespace. The difference between the sum of all sidecar container memory and a single node’s ztunnel memory is the actual payoff the benchmarks talk about, and on your workload it can differ from the widely quoted ~70%.
Bottom line
Ambient removes the main argument teams have used for years to put off adopting service mesh: mTLS and basic traffic visibility now cost one ztunnel per node instead of an Envoy process per pod, and turn on without restarting a single application. The trade-off is that the model is now two layers, and you have to consciously decide which services need a waypoint and L7, and which are fine with just L4. That’s a fair trade — complexity is now proportional to what a service actually needs, instead of being spread evenly across the whole cluster.