SevyDevy

Guide · Backend

Service Mesh Explained: Sidecars, mTLS, Retries and When You Need One

23 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Service mesh explained in one line: small proxies, programs that pass traffic along, sit between your services and add encryption, retries, timeouts and metrics without code changes.
  • Proxies either run beside every app copy as a sidecar container, or are shared per machine in Istio's ambient mode. Ambient reached general availability, meaning ready for production, in Istio 1.24 in November 2024.
  • mTLS, mutual TLS, means both sides show certificates, so each service proves who it is. The mesh issues and rotates those certificates for you, and they last 24 hours by default in Istio and Linkerd.
  • Mesh retries are powerful and risky. In our Envoy 1.39.1 test, one retried POST reached the backend twice, and two retrying layers turned one failing call into 9 attempts.
  • Beginners can stop after the sidecar vs ambient section. Engineers get Envoy 1.39.1 test results for mTLS, retries, timeouts and a 90/10 canary split, measured latency, and a decision guide.

What goes wrong when microservices talk to each other over the network?

Calls between services fail in ways a function call never does. They time out, reach a copy of the service that just stopped, travel unencrypted, and leave no trace of where the time went. A service mesh answers all of these in one layer.

Picture a common setup. A food-delivery app runs on Kubernetes, and its orders service calls a payments service for every checkout. The setup is made up, but each failure below is real and appears in our tests later.

  • A payments copy restarts during a deploy, and a few checkouts fail with errors the user can see.
  • Payments gets slow, orders waits with no time limit, and soon every orders thread is stuck waiting.
  • Traffic inside the cluster is plain HTTP, so anything that can read the network can read card tokens.
  • A new payments version ships to everyone at once, and a bug hits every user instead of a few.
  • Checkout feels slow, and nobody can say whether orders, payments or the network is to blame.

Each team can fix these in its own code. That works for two services in one language. It gets hard with forty services in four languages, because every team must build the same retries, certificates and metrics, and keep them identical.

Which words do you need before learning about a service mesh?

You need six words. Each one comes back in every later section.

  • Proxy: a program that receives a network request and forwards it to the real destination. It can change, retry or record the request on the way.
  • Sidecar: a helper container that runs inside the same Kubernetes pod as your app, like a sidecar attached to a motorbike. A pod is the smallest unit Kubernetes runs.
  • Data plane: the proxies that actually carry the traffic.
  • Control plane: the program that tells every proxy what to do and hands out certificates. In Istio it is called istiod.
  • mTLS (mutual TLS): normal TLS, the encryption behind HTTPS, where the client also shows a certificate. Both sides prove their identity, not just the server.
  • L4 and L7: Layer 4 means connections and bytes, with no idea of HTTP. Layer 7 means HTTP requests, with paths, headers and status codes.

What is a service mesh and how does it work?

A service mesh is a set of proxies that every service-to-service call passes through, plus a control plane that configures them. Your app keeps making plain calls, and the proxies add security and reliability on the way.

In the classic sidecar design, each app pod gets an extra proxy container. Network rules inside the pod send all its traffic through that proxy. In Istio, these rules come from istio-init, an init container that runs once before the app starts, or from a per-node CNI agent, as of Istio 1.31.

Follow one checkout call in the diagram. The orders app thinks it is calling payments directly. Its sidecar intercepts the call, opens an mTLS connection to the payments sidecar, and the payments sidecar hands the request to the payments app.

One checkout call through a sidecar mesh

Notice that the two apps only ever speak plain HTTP to their own sidecar. Encryption, identity checks and retries happen between the sidecars, configured by the control plane.

Diagram connections:

  • Orders app to Orders sidecar proxy (plain HTTP inside pod)
  • Orders sidecar proxy to Payments sidecar proxy (mTLS across network)
  • Payments sidecar proxy to Payments app (plain HTTP inside pod)
  • Control plane (istiod) to Orders sidecar proxy (routes, retry rules)
  • Control plane (istiod) to Payments sidecar proxy (certificates, policy)
  • Payments sidecar proxy to Metrics and traces (metrics)
  • Orders app: Calls payments over plain HTTP
  • Orders sidecar proxy: Applies retries, timeouts, routing
  • Payments sidecar proxy: Checks caller identity, records metrics
  • Payments app: Gets a normal HTTP request
  • Control plane (istiod): Sends config and certificates to proxies
  • Metrics and traces: Request counts, errors, latency

Think of it like an office building where every company has its own reception desk. You hand a parcel to your desk, it checks the other desk's ID, sends the parcel, and logs it. The building manager gives every desk the same rule book.

The mapping: your desk is the orders sidecar, the other desk is the payments sidecar, the ID check is mTLS, and the building manager is the control plane. Where the picture breaks: real reception desks are slow, while a proxy adds well under a millisecond, as our measurements show later.

The most common open-source meshes are Istio and Linkerd. Both are graduated projects of the CNCF, the foundation that also hosts Kubernetes. Istio's sidecars use the Envoy proxy, while Linkerd uses its own small proxy written in Rust.

What does a service mesh give you that plain Kubernetes does not?

Kubernetes finds and load balances your pods; a mesh controls what happens on each call. Service discovery tells orders where payments lives. The mesh decides how the call is secured, retried, timed, split and measured.

What each layer handles for the orders to payments call
NeedPlain Kubernetes ServiceWith a service mesh
Find a healthy payments podYes, through DNS and endpointsYes, plus smarter per-request load balancing
Encrypt traffic between podsNo, unless the app does TLS itselfmTLS between proxies, certificates rotated automatically
Prove which service is callingNo built-in caller identityEach workload gets a certificate tied to its service account
Retries and timeoutsWritten in each appSet in config, applied by the proxy
Send 10 percent to a new versionRough, by changing pod countsExact request weights, such as 90 and 10
Per-service request metricsOnly what each app exportsRequest rate, errors and latency from every proxy

A service account is the Kubernetes identity a pod runs as. The mesh turns it into a certificate, so payments can say: only the orders service may call me.

Sidecar vs ambient mesh: where should the proxy run in 2026?

A sidecar mesh puts one full proxy in every pod, while Istio's ambient mode shares proxies per machine and adds HTTP-level proxies only where you ask for them. Both give you mTLS; they differ in cost, isolation and features.

Sidecars have real costs. Each pod pays for a proxy sized for its busiest moment. Sidecars are added when a pod is created, so joining the mesh or upgrading the proxy means restarting your pods.

How Istio ambient mode works

Ambient mode splits the proxy into two layers. A ztunnel, short for zero-trust tunnel, runs once per node, which is one machine in the cluster. Zero trust means no caller is trusted just for being inside the network. It handles only L4 work: mTLS, simple connection-level allow or deny rules, and connection metrics.

The ztunnel does not read HTTP at all. For retries, timeouts, traffic splitting or path-based rules, you deploy a waypoint proxy. A waypoint is an Envoy deployment that runs outside your app pods, often one per namespace. A namespace is a named group of resources inside a cluster.

Ztunnels talk to each other over HBONE, an mTLS tunnel built on the HTTP CONNECT method. You add a namespace with the label istio.io/dataplane-mode=ambient, and Istio's docs say no app restart is needed.

The same call in Istio ambient mode

Notice there is no proxy inside either app pod. The node ztunnels always encrypt; the waypoint appears only because payments needs HTTP features such as retries and weights.

Diagram connections:

  • Orders app pod to ztunnel on node A (plain traffic)
  • ztunnel on node A to Waypoint proxy (Envoy) (HBONE mTLS tunnel)
  • Waypoint proxy (Envoy) to ztunnel on node B (HBONE mTLS tunnel)
  • ztunnel on node B to Payments app pod (plain traffic)
  • Control plane (istiod) to Waypoint proxy (Envoy) (L7 config)
  • Control plane (istiod) to ztunnel on node A (identities, L4 policy)
  • Orders app pod: No sidecar inside
  • ztunnel on node A: L4 only: mTLS, identity, TCP metrics
  • Waypoint proxy (Envoy): L7: retries, timeouts, weights, HTTP rules
  • ztunnel on node B: Unwraps the tunnel for the local pod
  • Payments app pod: No sidecar inside
  • Control plane (istiod): Configures ztunnels and waypoints
Three ways to run mesh proxies, as of 25 October 2026
Istio sidecar modeIstio ambient modeLinkerd
Where the proxy runsEnvoy in every podztunnel per node, optional Envoy waypointRust micro-proxy in every pod
mTLSYes, enforced by mesh policyYes, from ztunnel, no waypoint neededOn by default for TCP between meshed pods
HTTP retries, timeouts, weightsYes, in every sidecarOnly through a waypointYes, configured on routes
Joining the meshLabel namespace, then restart podsLabel namespace, no restartAnnotate, then restart pods
Multiple clustersSupported; Istio's feature list marks multicluster BetaMulti-network is Beta, not for production yetSupported; clusters share one root certificate
If one proxy is compromisedOnly that pod's identity leaksIdentities of every workload on that node leakOnly that pod's identity leaks

Two details are easy to miss. Since Linkerd's open-source project stopped shipping stable builds in February 2024, its stable releases come from vendors, while the project ships edge builds. On ambient waypoints, Istio's feature list marks VirtualService as Alpha, while routes from the Gateway API, a newer Kubernetes routing standard, are Stable.

How does mTLS work between services in a service mesh?

Each workload gets a short-lived certificate that names its identity, and the proxies check each other's certificates on every new connection. The app never touches a key.

The identity follows the SPIFFE standard, an open spec for workload identity. A SPIFFE ID is a URI such as spiffe://cluster.local/ns/shop/sa/orders. Istio builds it from the trust domain, the namespace and the service account.

The certificate carries that URI in its subject alternative name field, the part of a certificate that lists the names it is valid for. The X.509-SVID spec says a certificate must hold exactly one such URI.

  • Issuing: the Istio agent in each pod makes a private key and asks istiod to sign it, as of 1.31.
  • Rotating: the agent requests 24-hour certificates by default and renews them before expiry. Linkerd's docs also give 24 hours.
  • Enforcing: Istio's PeerAuthentication policy has three modes. PERMISSIVE accepts mTLS and plain text, STRICT accepts only mTLS, and DISABLE turns it off.
  • Default: when no mode is set, Istio uses PERMISSIVE, so plain-text callers still get through until you switch to STRICT.

What we saw when we ran it

We could not run a Kubernetes cluster in our test box, so we rebuilt the mechanism with two Envoy 1.39.1 proxies. One acted as the orders sidecar and one as the payments sidecar, with a throwaway certificate authority and SPIFFE IDs.

The payments proxy was told to accept only the orders identity. This is the key part of its config.

yaml
# Payments sidecar listener: require a client certificate
# and accept only the orders SPIFFE ID (Envoy 1.39.1).
require_client_certificate: true
common_tls_context:
  validation_context:
    trusted_ca: {filename: certs/ca.crt}
    match_typed_subject_alt_names:
    - san_type: URI
      matcher: {exact: "spiffe://cluster.local/ns/shop/sa/orders"}
Four callers against the payments proxy (Envoy 1.39.1, run on 25 October 2026)
CallerResultHow you can check it
Orders app through its own proxyHTTP 200Payments saw URI=spiffe://cluster.local/ns/shop/sa/orders in x-forwarded-client-cert, a header Envoy adds to name the caller
curl with no client certificateRefused during the TLS handshakecurl exit code 35, no HTTP status at all
Valid certificate from the same CA, wrong identityRefusedcurl exit code 56; Envoy stat ssl.fail_verify_san went to 1
Plain HTTP to the mTLS portRefusedcurl exit code 52, empty reply

Row three is the one to remember. Encryption alone is not access control. A certificate from your own mesh is not enough; the receiving side must also check which identity it names, which is what Istio's AuthorizationPolicy does in a real cluster.

How do retries and timeouts work in a service mesh?

The caller's proxy watches each response, retries failures that match a rule, and gives up when a time budget runs out. Your code sees one final answer.

We put a test payments service behind Envoy 1.39.1. On one path, every odd-numbered request returns 503, meaning service unavailable. On another, it sleeps for 3 seconds before replying. This is the route rule we used.

yaml
# Envoy 1.39.1 route: total budget 1s, up to 2 retries,
# each attempt limited to 0.4s, retry on any 5xx or no response.
route:
  cluster: payments
  timeout: 1s
  retry_policy:
    retry_on: "5xx"
    num_retries: 2
    per_try_timeout: 0.4s
Retry and timeout behaviour through Envoy 1.39.1 (our run, 25 October 2026)
TestWhat the caller sawWhat Envoy's counters showed
10 GETs to the flaky path, no retry policy5 errors (503) and 5 successesupstream_rq_503 went up by 5
10 GETs to the flaky path, 2 retries10 successesupstream_rq_retry went up by 10
Slow path, 1s timeout, no retries504 upstream request timeout after 1.00supstream_rq_timeout went up by 1
Slow path, 1s timeout, 0.4s per try504 after 1.00s, not after 3 tries of 3sTwo per-try timeouts, then the 1s budget ran out

The fourth row shows a rule from Envoy's docs: the route timeout includes all retries. Retries never stretch your 1-second budget. Without a per-try timeout, a slow first attempt uses the whole budget and nothing is retried.

The retry that charged twice

Now a POST to the payments path, which also fails on odd-numbered attempts. Pause and predict: the caller gets a 200, but how many payment requests did the payments service receive?

It received two. The first attempt reached the app, which returned 503; Envoy retried, and the second attempt succeeded. Envoy's router docs list no rule that skips POST, and our run confirms it retried one.

What just happened? A 503 does not prove the work was not done. If the payments app had charged the card before failing, the retry would charge again. Linkerd's docs say plainly that retries should only be used on idempotent methods, ones that are safe to repeat.

How retries multiply across layers

Next we chained two Envoy proxies, each set to 2 retries, in front of a path that always fails. One client call produced 9 attempts at the failing service. A single layer produced 3. Each layer multiplies, so by the same arithmetic three such layers would give 27.

Linkerd's docs warn that constantly retried requests can take a struggling service down instead of letting it recover. The guide Retry, Backoff and Jitter: How to Avoid a Retry Storm covers budgets that cap this. In a mesh, retry at one layer only, usually the caller closest to the user.

What Istio and Linkerd do by default

Istio retries HTTP requests twice by default and sets no request timeout, although plain Envoy defaults to a 15-second route timeout. The 1.31 reference lists the default triggers as connection failures, refused streams, and the gRPC statuses unavailable and cancelled. gRPC is a remote-call framework that runs over HTTP/2.

That list has no HTTP status codes. Reading the 1.31 source, the default also enables Envoy's retriable-status-codes trigger but supplies no codes, so we found nothing that retries a plain 503. We read this in source and did not run it in a cluster, so check your own version.

Linkerd configures retries and timeouts with annotations such as retry.linkerd.io/http and timeout.linkerd.io/request, on routes or services. Since Linkerd 2.16, a request that times out can be retried. Both apply on the calling side, so the caller must be in the mesh.

How does traffic shifting work for a canary release in a mesh?

The proxy picks a destination for each request by weight, so you can send a fixed share of real traffic to a new version. A canary release means sending a small share to the new version first and watching it.

We ran Envoy 1.39.1 with two payments versions weighted 90 and 10 and sent 1,000 requests twice. Version 1 got 914 then 899, and version 2 got 86 then 101. The split is applied request by request, so short runs wobble around the target.

In Istio you write the same rule as a VirtualService with weights, plus a DestinationRule that defines the two versions by pod label. We checked this YAML with istioctl 1.31.3 validate, which checks the schema offline; we did not deploy it to a cluster.

yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: payments
  namespace: shop
spec:
  hosts:
  - payments.shop.svc.cluster.local
  http:
  - timeout: 1s
    retries:
      attempts: 2
      perTryTimeout: 400ms
      retryOn: 5xx
    route:
    - destination:
        host: payments.shop.svc.cluster.local
        subset: v1
      weight: 90
    - destination:
        host: payments.shop.svc.cluster.local
        subset: v2
      weight: 10

Linkerd does the same with an HTTPRoute whose backend list carries weights, and its docs show the same 90 and 10 example. Whichever mesh you use, move the weight in steps such as 10, 50 and 100, checking error rate and latency for version 2 at each step.

What can a service mesh show you without changing your code?

Every proxy counts requests, errors and latency for every call it carries, so you get the same core metrics for every service. Tracing is the exception: apps must still pass a few headers along.

Istio's standard metrics include istio_requests_total and istio_request_duration_milliseconds. Labels such as source_workload, destination_workload and response_code let you ask how many orders-to-payments calls failed. Linkerd's telemetry gives success rate, request rate and latency percentiles, with no developer work.

A trace is the full path of one request across services, built from timed pieces called spans. Proxies send spans automatically, but Istio's docs say apps must forward headers such as traceparent and x-request-id to join them into one trace.

One counter detail caught us out. After the retry tests, Envoy's upstream_rq_503 counter showed only the 5 errors the client saw. The 11 retried 503s appeared under retry.upstream_rq_503, which matches Envoy's stats docs. Alert on both, or retries will hide a sick service.

How much does a service mesh cost in latency and effort?

Each proxy hop adds a fraction of a millisecond, but the bigger cost is people's time. Someone must upgrade, debug and secure the mesh itself.

We measured 3,000 keep-alive GET requests, three times, on a 2-core Linux box over loopback, the machine's internal network. Direct calls had a median of 0.50 to 0.72 milliseconds. Through one Envoy, it was 0.76 to 0.84.

Through two Envoys with mTLS between them, the sidecar-to-sidecar path, the median was 0.96 to 1.17 milliseconds. Across all three paths, the 99th percentile, the slowest 1 in 100 requests, ranged from 2.6 to 6.7 milliseconds and was too noisy to compare.

Treat these as a rough size, not a benchmark. Real clusters add real network hops, bigger payloads and busier CPUs. If your service answers in 50 milliseconds, half a millisecond rarely matters; if a request crosses ten services, the hops add up.

  • Memory and CPU: sidecar mode runs one proxy per pod. Istio's own comparison rates ambient's average resource cost as small and sidecar's as large.
  • Upgrades: Istio supports each minor version for a limited time. Istio 1.29 support ended on 12 October 2026, so plan an upgrade every few months.
  • Security fixes: Istio published two security bulletins in October 2026 alone, each needing a patch release.
  • Debugging: a failed call can now fail in the app, either proxy, the control plane or the policy, and the team must know how to tell which.

When does Istio or Linkerd earn its complexity?

A mesh earns its keep when many services, often in several languages, need the same encryption, identity and traffic rules, and a team can own it. For a handful of services, a library or a gateway is usually enough.

Do you need a service mesh yet?

Notice that most paths end at something simpler than a mesh. The mesh wins only when the need is cluster-wide and someone can run it.

Diagram connections:

  • Need mTLS between pods? to A team to run it? (Yes)
  • Need mTLS between pods? to Many services or languages? (No)
  • Many services or languages? to Use a library (No, few services)
  • Many services or languages? to A team to run it? (Yes)
  • A team to run it? to Library plus gateway (No, keep it simple)
  • A team to run it? to Ambient or Linkerd (Yes, mainly security)
  • Ambient or Linkerd to Full L7 mesh (Later, if L7 rules needed)
  • Need mTLS between pods?: Compliance or zero-trust requirement
  • Many services or languages?: Roughly a dozen or more, several stacks
  • A team to run it?: Upgrades, alerts, on-call for the mesh
  • Use a library: Retries, timeouts, breakers in code
  • Library plus gateway: Shared client library; app TLS if required
  • Ambient or Linkerd: Start with encryption and identity
  • Full L7 mesh: Waypoints or sidecars for traffic rules
The simplest tool for each need
Your real needOften enoughWhen a mesh wins
Retries and timeouts for 3 services in one languageA shared HTTP client libraryWhen 20 services in 4 languages must behave the same
Rate limits and auth for outside trafficAn API gateway at the edgeWhen the same rules must apply between internal services
Encrypt traffic between podsApp-level TLS, if you have few servicesWhen certificates for every workload must rotate daily
Safe rolloutsRolling deploys and feature flagsWhen you need exact request weights or header-based routing
Metrics per serviceOpenTelemetry in each appWhen you need identical metrics from every call, including old services

Advice, not a rule: if you choose a mesh, start with mTLS in PERMISSIVE mode, watch the metrics, then switch to STRICT one namespace at a time. Add HTTP rules later, one service at a time. The guide Circuit Breaker Pattern: Fail Fast When a Dependency Is Sick covers the in-code option.

Between the two, Istio offers two data-plane modes and Envoy's wide feature set. Linkerd's proxy is built only for the mesh and turns mTLS on by default. Pick the one whose docs answer your top three needs, then test it on one non-critical service first.

How do you explain a service mesh in a system design interview?

Lead with the problem, then the mechanism, then the trade-off. Use these steps as your frame.

  1. Name the problem

    Many services, many languages, and every call needs encryption, retries, timeouts and metrics. Doing it in each codebase drifts.

  2. Describe the data plane

    Proxies carry every call: a sidecar per pod, or per-node ztunnels plus waypoints in Istio ambient mode.

  3. Describe the control plane

    It pushes routing rules to proxies and issues short-lived certificates with SPIFFE identities for mTLS.

  4. Walk one request

    Orders calls payments, its proxy opens mTLS, checks identity, applies a 1-second timeout and 2 retries, and records metrics.

  5. State the risks

    Retries can repeat non-idempotent calls and multiply across layers. Each hop adds a little latency, and someone must run the mesh.

  6. Say when you would skip it

    With a few services in one language, a client library plus an API gateway is simpler. Add a mesh when the need is cluster-wide.

Questions people ask

What is a service mesh in simple words?
A service mesh is a layer of proxies that every call between your services passes through. A control plane configures those proxies. Together they add encryption, caller identity, retries, timeouts, traffic splitting and metrics, without changes to your application code. Istio and Linkerd are the best-known open-source meshes, and both are graduated CNCF projects.
Is a service mesh the same as an API gateway?
No. An API gateway is one front door for traffic coming from outside, handling things like authentication and rate limits for clients. A service mesh handles traffic between your own services inside the cluster. Many systems use both: a gateway at the edge and a mesh for internal calls. With only a few internal services, the gateway alone is often enough.
What is the difference between sidecar and ambient mode in Istio?
Sidecar mode runs an Envoy proxy in every pod. Ambient mode runs a shared ztunnel on each node for mTLS and connection-level rules, and adds Envoy waypoint proxies only where you need HTTP features. Ambient reached general availability, meaning ready for production, in Istio 1.24 in November 2024. It needs no pod restarts to join, but some features, such as EnvoyFilter, are sidecar only.
Does Linkerd have an ambient or sidecarless mode?
No, not as of 25 October 2026. Linkerd's architecture docs describe only sidecar proxies, and from Linkerd 2.20 the proxy runs as a Kubernetes native sidecar container by default. Its proxy is a small Rust program built only for the mesh, not a general-purpose proxy like Envoy. Check the Linkerd release notes for later changes.
Does mTLS in a service mesh replace authorization?
No. mTLS encrypts the connection and proves which workload is calling, but by itself it lets any workload in the mesh connect. In our Envoy test, the identity check had to be configured separately to block a valid certificate with the wrong identity. In Istio, that job belongs to AuthorizationPolicy rules that allow only named callers.
Can service mesh retries cause duplicate payments?
Yes, if you retry requests that are not safe to repeat. In our Envoy 1.39.1 test, one POST that first got a 503 reached the payments service twice. A 503 does not prove the work was not done. Retry only idempotent calls, or make writes safe with idempotency keys, and keep retries at one layer.
How much latency does a service mesh add?
Usually a fraction of a millisecond per hop at the median. On our 2-core test box over loopback, two Envoy proxies with mTLS between them added roughly 0.25 to 0.6 milliseconds at the median compared with direct calls. Real clusters differ, so measure your own median (p50) and p99 before and after enabling the mesh on one service.

Sources and further reading

Practise this with Lala

Practise