System designDeep dive10 min
Service mesh & sidecars
Every service needs the same things from the network: encryption, identity, retries, timeouts, load balancing and metrics. A service mesh moves them out of each service's code into a proxy beside it, configured centrally, so sixty services in four languages behave alike. It costs a proxy hop on every call and a new system to run, and newer designs try to shrink that bill.
1 · The problem
Every service needs the same network features, in every language
A hotel booking platform has 60 services written in Java, Go, Python and Node. Every call between them should be encrypted and authenticated, have a timeout and a sensible retry, be spread across healthy instances, and show up in the metrics. Each team built those features with a library for its language:
Need | Where it lives without a mesh | What drifts |
|---|---|---|
Mutual TLS and service identity | A TLS library and certificate handling per service | Rotation scripts, expired certificates |
Timeouts and retries | An HTTP client wrapper per language | Different defaults in every service |
Load balancing across instances | A discovery client library | Old versions balancing per connection |
Metrics per call | Instrumentation in each codebase | Different names and buckets |
Sending 10% to a new version | Custom routing code | Usually not built at all |
The real cost is change. Fixing a retry bug means a release of four libraries and sixty services. Deploying library upgrades across a large service architecture can be incredibly painful. A service mesh takes the other route: put the networking in a separate process beside each service, the same process for every language, and upgrade it without touching application code.
2 · The sidecar
A proxy beside each service handles its traffic, and a control plane configures them all
In the classic design, every instance of every service gets a sidecar proxy in the same pod. Rules in the pod's network setup redirect all its inbound and outbound traffic through the proxy, so the application talks plain HTTP to what looks like the service it called, and never knows the proxy is there:
One call, two proxies: the caller's sidecar picks an instance, applies timeouts and retries and opens mTLS; the receiver's checks identity and policy. Both report metrics.
The proxies are the data plane: they carry every request. The control plane never touches requests; it configures the proxies. In Istio, the control plane (istiod) turns routing rules into Envoy configuration and pushes it to every sidecar at runtime, tracks service discovery, and acts as a certificate authority for the proxies' identities. The protocol Envoy uses to receive that configuration, xDS, has become a common language: gRPC can speak it directly.
Linkerd follows the same split with its own small proxy written in Rust, built only for the mesh rather than as a general proxy. Kubernetes now supports sidecars natively, stable since v1.33: a sidecar starts before the application container and, when the pod shuts down, is stopped only after the application has stopped, so the application never loses its network on the way in or out.
3 · What it does
The mesh gives every call identity, policy, resilience and telemetry
With the proxies in place, the platform team configures behavior once, for all 60 services:
Feature | How the mesh provides it | At the hotel platform |
|---|---|---|
Identity and encryption | Each workload gets an X.509 certificate; agents rotate keys automatically; proxies use mTLS | Pricing knows a call came from booking, not just from an IP |
Authorization | Policies by service identity, enforced in the proxy | Only booking and search may call pricing |
Load balancing | Per request; Istio's default picks the less busy of two random instances | New pricing instances get traffic at once |
Resilience | Timeouts, retries, circuit breakers configured per destination | Pricing calls: 2-second timeout per try |
Traffic splitting | Weighted routes between versions | 75% to pricing v1, 25% to v2 |
Telemetry | Metrics for every call, from the proxies | Error rate between any two services |
Turning on mutual TLS (TLS in which both sides present a certificate, so each knows who the other is) in a running system has a migration path built in. Istio's PERMISSIVE mode lets a workload accept both mTLS and plain text while callers are still being moved into the mesh; once every caller has a proxy, STRICT mode accepts mutual TLS only. That makes "encrypt everything inside the data center" a configuration change rather than sixty code changes (Encryption in transit & at rest).
Per-request load balancing also fixes a problem plain Kubernetes Services have with long-lived connections: a connection-level balancer never moves an open connection to new instances, while a proxy chooses an instance for each request (Service discovery & registration). Traffic splitting lets the mesh run a canary without touching the deployment (Deployment strategies).
4 · The costs
Every call pays two proxy hops, and the mesh is one more system to run
Nothing in the path is free. Each call now passes through two extra proxies, and each proxy needs CPU and memory. Istio publishes its own measurements: in Istio 1.24, at 1,000 requests a second with 1 KB payloads, one sidecar with two worker threads uses about 0.20 vCPU and 60 MB of memory. Memory grows with the amount of routing configuration the proxy holds.
At the hotel platform's 400 pods, memory alone adds up:
Istio's measured figures: about 60 MB per sidecar, about 12 MB per ztunnel node proxy in ambient mode (below). The platform's 400 pods on 40 nodes are the example's.
Two subtler costs catch teams out:
- Retries multiply. Istio retries a failed HTTP request twice by default, so up to three attempts per hop. If booking calls pricing, pricing calls rates, and rates calls inventory, a failing inventory can receive 3 × 3 × 3 = 27 attempts for one user request, more if the applications retry too. Set retries in one place and budget them (Timeouts, retries, backoff & jitter).
- Tracing still needs the application. Proxies send spans for every call, but they can't know which outgoing request belongs to which incoming one. Each service must copy the trace headers across:
In Istio, applications must forward headers such as traceparent and x-request-id from each incoming request to the outgoing requests it triggers, or the spans can't be joined into one trace (Observability: metrics, logs & traces).
And the mesh itself must be operated: upgraded, scaled, monitored and understood when it misbehaves. A wrong routing rule pushed by the control plane reaches every proxy, so mesh configuration deserves the same staged rollout as code (Feature flags & configuration).
5 · Without sidecars
Newer designs move the proxy off the pod, or into the library
The per-pod sidecar is where most of the cost lives, so meshes now offer alternatives:
- Ambient mode in Istio splits the job in two. A per-node proxy, ztunnel, written in Rust, handles only layer 4: mutual TLS, authentication, layer 4 authorization and telemetry, without parsing HTTP. Only where layer 7 features are needed, such as HTTP routing, retries or authorization by path, is a waypoint proxy added, running outside the application pods.
- Proxyless gRPC removes the proxy altogether for gRPC services: the gRPC library speaks xDS itself, receiving routing, load balancing and mTLS configuration from the same control plane.
Model | Proxies | Per-request cost | Limits |
|---|---|---|---|
Sidecar | One per pod | About 0.20 vCPU, 60 MB per sidecar at 1,000 req/s | Most capable; most overhead |
Ambient, layer 4 only | One ztunnel per node | About 0.06 vCPU, 12 MB per ztunnel | mTLS and layer 4 policy only |
Ambient with waypoints | Per node, plus shared waypoints | Waypoint ~0.25 vCPU, 60 MB | Full features where enabled |
Proxyless gRPC | None | Inside the application | gRPC only; library must support xDS |
Istio's own tests of proxyless gRPC found a marginal latency increase over plain gRPC, which it called a massive improvement over going through Envoy. The modes also mix: sidecar and ambient workloads can coexist in one mesh, so a platform can start with layer 4 encryption everywhere and add layer 7 features only for the services that need them.
6 · In practice
Adopt a mesh for many services and languages, not for a few
Situation | Better choice |
|---|---|
A handful of services, one language | A shared library, or the platform's load balancer |
Dozens of services, several languages, encryption required | A mesh |
Mainly need mTLS and identity everywhere | A layer 4 mesh, such as ambient mode without waypoints |
All gRPC, latency-sensitive | Proxyless gRPC with an xDS control plane |
Traffic from the internet | An API gateway at the edge, which a mesh doesn't replace |
A mesh handles traffic between services; the edge still needs a gateway for authentication of users, rate limits and public routing (Reverse proxies & API gateways). For the hotel platform, the checklist:
- Start in permissive mode, move every service into the mesh, then switch to strict mTLS.
- Write authorization policies by service identity, not by network address.
- Configure timeouts and retries in one place, and remove duplicate retries from application code.
- Forward trace headers in every service.
- Budget the proxies' CPU and memory, and consider ambient or proxyless modes where the overhead matters.
- Roll out mesh configuration in stages, like any other production change.
Check yourself
5 questions
Takeaways
Remember this
- A service mesh moves encryption, identity, retries, timeouts, load balancing and telemetry out of application code into proxies, the same for every language.
- Proxies form the data plane; a control plane pushes them routing, discovery and certificates.
- Meshes enable strict mTLS by service identity, per-request balancing and traffic splitting as configuration, not code.
- Each call pays two proxy hops and each proxy costs CPU and memory; retries must be set in one place, and apps must still forward trace headers.
- Ambient (per-node) and proxyless designs cut the per-pod cost, and mixing modes lets features be added only where needed.