What a sidecar mesh actually buys and what it costs, the four Envoy objects every mesh CRD renders into, what discovery adds on top of kube-dns, and the port name that silently disables every L7 feature.
The application opens an ordinary connection. Everything after that is interception the app never sees.
Because capture is transparent, the app cannot tell you what the mesh did. That is why every debugging path here goes through the proxy rather than the application log.
The diagram above is the high level: what the pieces are. These two are the ones you want when something is wrong — what is inside one of those boxes, and the path a request really takes through them.
What the parts are, and which decisions actually matter.
A service mesh is two things: a data plane of proxies — one per pod, or one per node — that every request passes through, and a control plane that configures them. The application is not modified and usually does not know the proxy is there.
What you get, and the reason it is worth the weight: mTLS everywhere without touching application code, retries and timeouts and circuit breaking as policy rather than as library code in six languages, traffic splitting for canaries, and uniform golden-signal telemetry for every hop in the system.
Ten services, one language, one team: a shared HTTP client library with retries and timeouts gets most of the benefit at none of the cost. The mesh wins when the count of services and languages is high enough that "put it in the library" stops being one change and becomes six.
Ambient / sidecar-less modes change this calculation. Istio's ambient mode moves mTLS and L4 telemetry to a per-node component and makes the L7 proxy opt-in per namespace, which removes the per-pod sidecar cost for workloads that only need encryption and metrics.
Istio, Gloo, Consul, Gateway API implementations and most of the ecosystem use Envoy as the data plane. Understanding four of its objects makes mesh debugging tractable, because every mesh CRD ultimately renders into them:
| Object | Answers |
|---|---|
| Listener | What port am I accepting on? |
| Route | Given this request, which cluster? |
| Cluster | A named upstream, with its load-balancing and outlier policy |
| Endpoint | The actual pod IPs behind that cluster |
xDS is the protocol that streams these from the control plane to every proxy. When someone says "the config has not propagated", they mean a proxy's xDS state is stale — and that is directly observable rather than a matter of opinion.
Do not reason about what the CRDs should have produced. Ask the proxy what it actually has.
Kubernetes service discovery is already complete: CoreDNS resolves
svc.ns.svc.cluster.local to a ClusterIP, and kube-proxy load-balances to the
EndpointSlice. A mesh does not replace that — it intercepts after it.
What changes: the proxy has the full endpoint list rather than an iptables rule, so it can do things kube-proxy cannot — least-request instead of random, locality-aware routing that prefers same-zone endpoints and fails over to another zone only when the local ones are unhealthy, outlier detection that ejects an endpoint returning 5xx, and per-request retries with a budget.
Cross-zone traffic costs money and latency in every cloud. With zone labels on the nodes and locality load balancing on, same-zone requests stay in-zone — often a double-digit percentage of inter-AZ transfer removed for one config change.
| Istio | Linkerd | |
|---|---|---|
| Proxy | Envoy (C++), very configurable | linkerd2-proxy (Rust), purpose-built, small |
| Surface | Large — many CRDs, many knobs | Deliberately small |
| Resource cost | Higher per sidecar | Notably lower |
| Sidecar-less | Ambient mode | — |
| Strength | Anything you can express, you can configure | Fewer ways to get it wrong |
The decision is rarely about capability, because both do the core job. It is about whether you want the configurability and will staff for it, or want the smallest thing that provides mTLS, retries and golden signals.
The Kubernetes Gateway API is the successor to Ingress and now has a service-mesh profile (GAMMA). Both Istio and Linkerd implement it. For new configuration it is worth preferring Gateway API resources over vendor CRDs where they cover your case — the config outlives the mesh you picked.
Mesh debugging has one rule: stop reasoning about the CRDs and ask the proxy. The CRDs are intent; the proxy's xDS state is what is actually happening, and the gap between them is where the bug lives.
| Symptom | Likely cause | Command |
|---|---|---|
503 with UC/UF flags | Upstream refused or unreachable | istioctl proxy-config endpoint — is the list empty? |
503 NR (no route) | No VirtualService matches this host/port | istioctl proxy-config route |
| Works without the sidecar, fails with it | Port naming, or a protocol the mesh mis-detected | Service port must be named http, grpc, tcp… |
| mTLS handshake failures | PeerAuthentication STRICT with a non-mesh client | istioctl x describe pod <p> |
| Config change did nothing | Proxy has stale xDS | istioctl proxy-status — anything not SYNCED |
| Some pods not meshed | Injection label missing, or the pod predates it | kubectl get ns -L istio-injection; then restart |
| Intermittent 503 on deploy | No graceful drain — in-flight requests cut | terminationGracePeriodSeconds and a preStop sleep |
Istio infers protocol from the Service port name. A port named
web is treated as plain TCP, so HTTP routing, retries, and L7 telemetry silently do
nothing — no error, just features that are quietly absent. Name it http, or use
appProtocol.
Envoy tags every request with response flags — UF upstream failure, UH no healthy upstream, NR no route, UO outlier-ejected, URX retry limit. They distinguish 'nothing to send it to' from 'nowhere to route it' immediately, which is the fork you would otherwise spend twenty minutes narrowing by hand.
| Command | What it answers |
|---|---|
istioctl analyze -A | Misconfiguration, before it becomes an incident |
istioctl proxy-status | Which proxies have stale config |
istioctl x describe pod <p> | Policies, mTLS mode and routes for one pod |
istioctl proxy-config route <p> | The routes this proxy actually has |
istioctl proxy-config cluster <p> | Upstreams it knows about |
istioctl proxy-config endpoint <p> | Real pod IPs — empty means 503 |
istioctl proxy-config listener <p> | Ports it is accepting on |
kubectl logs <p> -c istio-proxy | Response flags — the two-character diagnosis |
kubectl get ns -L istio-injection | Which namespaces are meshed |
linkerd check | Linkerd's equivalent end-to-end health check |
linkerd viz stat deploy -n <ns> | Success rate, RPS and latency per workload |