⏱️ Lectura: 15 min
When a Kubernetes pod crashes mid-request, your application doesn’t need to know about it: a proxy running alongside each container can retry the call, open a circuit breaker, and encrypt the connection with mTLS before your code notices anything strange. That proxy is the central piece of a service mesh, the infrastructure layer that separates network logic from business logic in microservices architectures.
📑 En este artículo
- TL;DR
- What is a service mesh and why it matters
- How it works: the sidecar pattern
- Practical examples: installing Istio and seeing the sidecar in action
- Circuit breakers and retries: resilience without extra code
- Observability: metrics, traces, and logs without instrumenting code
- Getting started: from zero to your first mesh on Kubernetes
- Real-world use cases
- Common mistakes and gotchas
- Comparison with alternatives
- Going deeper: xDS, Ambient Mesh, and the sidecarless future
- Frequently Asked Questions
- References
In this guide, you’ll see how the sidecar pattern works, how to install Istio with Envoy on a real cluster, and in which cases that extra proxy isn’t worth it.
TL;DR
- You’ll understand what the sidecar pattern does and why it separates network logic from your application code.
- You’ll be able to install Istio with Envoy on a real Kubernetes cluster using istioctl.
- You’ll know how to write a
VirtualServiceto split traffic between two versions of the same service (canary release). - You’ll be able to enable automatic mTLS between services with a few lines of
PeerAuthentication. - You’ll configure a circuit breaker with
outlierDetectionwithout touching your service’s code. - You’ll learn to distinguish when a sidecar-based service mesh makes sense and when a sidecarless approach with eBPF is better.
- You’ll know the exact commands to verify that the sidecar is synced with the control plane.
What is a service mesh and why it matters
A service mesh is an infrastructure layer dedicated to handling communication between services within a microservices architecture. Instead of each service implementing its own retry logic, load balancing, encryption, and observability, that responsibility is delegated to a proxy that runs alongside each instance of the service.
The idea was born from a concrete problem: when a company moves from a monolith to dozens or hundreds of microservices, the communication between them (so-called east-west traffic) becomes as complex as the business logic itself. Each team ends up reimplementing circuit breakers, backoff retries, and latency metrics in whatever language they happen to use: Java, Go, Python. That duplicates work and multiplies bugs.
A service mesh solves this by moving that logic out of the application process and into a network proxy that intercepts every packet entering or leaving the container. Your application code keeps making normal HTTP or gRPC calls; the proxy decides whether to retry, encrypt, cut off traffic, or route it to a different version of the service.
How it works: the sidecar pattern
The central component of almost every modern service mesh is Envoy, a high-performance proxy written in C++ that Lyft open-sourced and that is now a graduated project of the Cloud Native Computing Foundation. Envoy isn’t installed as a central service: it’s injected as an additional container, the sidecar, inside each Kubernetes pod, alongside your application container.
This means a pod with an active service mesh doesn’t have one container, it has two: your application and the Envoy sidecar. All network traffic entering or leaving the pod first passes through iptables rules that Istio configures automatically, and those rules redirect traffic to the port where Envoy listens before letting it continue on its way.
Architecturally, a service mesh is split into two planes: the data plane, made up of all the Envoy proxies that actually move the packets, and the control plane, which in Istio is called Istiod and tells each Envoy which routing rules, certificates, and security policies to apply. That communication between Istiod and the Envoys happens over the xDS protocol, a gRPC API that originated with the Envoy project itself.
To illustrate the actual flow of a request between two services with mTLS enabled:
sequenceDiagram
participant App1 as Orders App
participant Sc1 as Envoy Sidecar 1
participant Sc2 as Envoy Sidecar 2
participant App2 as Payments App
App1->>Sc1: plain text HTTP request
Sc1->>Sc2: encrypted mTLS connection
Sc2->>App2: plain text HTTP request
App2-->>Sc2: response
Sc2-->>Sc1: encrypted response
Sc1-->>App1: plain text response
Note over Sc1,Sc2: Istiod distributed the certificates via xDS
Neither the Orders app nor the Payments app writes a single line of code for encryption: the two sidecars negotiate mTLS between themselves transparently.
Practical examples: installing Istio and seeing the sidecar in action
The first step is always to have the control plane running in the cluster. This installs Istiod and enables automatic sidecar injection:
curl -L https://istio.io/downloadIstio | sh -
cd istio-*
export PATH=$PWD/bin:$PATH
istioctl install --set profile=demo -y
kubectl label namespace default istio-injection=enabled
The istioctl install command deploys Istiod with the demo profile (intended for testing, with all components active). The kubectl label command marks the default namespace so every new pod automatically receives the sidecar container without you editing a single deployment manifest.
With the mesh active, the typical next step is to split traffic between two versions of the same service to do a canary release. This is defined with a VirtualService and a DestinationRule:
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: pagos-api
spec:
host: pagos-api
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: pagos-api
spec:
hosts:
- pagos-api
http:
- route:
- destination:
host: pagos-api
subset: v1
weight: 90
- destination:
host: pagos-api
subset: v2
weight: 10
The DestinationRule defines which pods belong to each version based on their labels; the VirtualService sends 90% of traffic to v1 (the stable version) and 10% to v2 (the candidate version). If v2 starts failing, you switch the weight to 0 without restarting any pod and without the client noticing anything different.
To enable strict mTLS between all services in the istio-system namespace (and by extension, the cluster), the object is a PeerAuthentication:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
With mode: STRICT, each sidecar rejects connections that aren’t encrypted with the certificate Istiod issued for that service. Certificates rotate automatically without any developer having to generate or copy a .pem file.
Circuit breakers and retries: resilience without extra code
Besides routing traffic, each sidecar can apply resilience policies that traditionally lived in libraries like Hystrix or resilience4j, but now run outside the application process. This is configured in the same DestinationRule you already used for the canary:
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: pagos-api-resiliencia
spec:
host: pagos-api
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 10
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 60s
maxEjectionPercent: 50
The outlierDetection block is the circuit breaker: if a pagos-api pod returns 5 consecutive 5xx errors within a 30-second window, Envoy pulls it out of load balancing for 60 seconds, without the client calling pagos-api having to implement that logic. The connectionPool limits how many pending requests and per-connection requests each pod accepts, preventing a slow service from cascading failures to the ones that depend on it.
This replaces patterns that used to require a language-specific library: a service in Go and another in Java can share exactly the same circuit-breaking policy, because it’s applied by the sidecar and not by each language’s runtime.
Observability: metrics, traces, and logs without instrumenting code
Since every Envoy sees absolutely all the traffic entering and leaving its pod, it automatically exposes metrics in Prometheus format: request rate, latency by percentile, response codes, and payload size, all tagged by service, version, and namespace. These metrics can be queried with a simple request:
kubectl exec pagos-api-7d4f9c-x2k1p -c istio-proxy -- \
curl -s localhost:15000/stats/prometheus | grep istio_requests_total
That command confirms, without guessing, that the sidecar is counting real requests: if the istio_requests_total counter goes up with each call, the mesh is active and measuring traffic. Combined with Grafana for dashboards and Jaeger for distributed tracing, a team gets end-to-end observability without adding a single instrumentation library to their services’ code; they just need to propagate the tracing headers (x-request-id, x-b3-traceid) between outgoing calls.
💡 Tip: if your app makes several outgoing calls within the same incoming request, you have to manually forward the incoming x-b3-* headers; the sidecar can’t invent the trace context that your own code doesn’t propagate.
Getting started: from zero to your first mesh on Kubernetes
To adopt a service mesh without breaking anything in production, the recommended order is this:
- Install Istio on a staging cluster with the
demoprofile, as in the block above. - Label only a test namespace with
istio-injection=enabled, never the whole cluster at once. - Redeploy the pods in that namespace (
kubectl rollout restart deployment) so they receive the sidecar: injection only happens when the pod is created. - Verify that the sidecar is running with
kubectl get pod <name> -o jsonpath='{.spec.containers[*].name}': you should seeistio-proxyalongside your app’s container. - Confirm that the sidecar is synced with the control plane using
istioctl proxy-status: theSYNCEDcolumn should saySYNCEDfor every proxy, notSTALE. - Enable mTLS in
PERMISSIVEmode first (it accepts both encrypted and unencrypted traffic at once) and only move toSTRICTafterward, once you confirm no service was left outside the mesh.
⚠️ Watch out: if you enable STRICT before every pod has the sidecar injected, those services without a sidecar are immediately cut off: they can’t speak mTLS because they have nothing to speak it with.
Real-world use cases
Teams that adopt a service mesh almost always do so for one of these four reasons:
- Zero-trust security: encrypting all internal traffic with mTLS without asking each team to manage certificates manually.
- Progressive rollouts: canary releases and blue-green deployments at the traffic level, using routing weights like the
pagos-apiexample. - Resilience: circuit breakers, exponential backoff retries, and timeouts configured centrally, instead of each microservice bringing its own library.
- Uniform observability: since all traffic passes through Envoy, latency metrics, error rates, and distributed traces come for free, without manually instrumenting each service.
A typical example: a payments team wants to launch a new version of their service but doesn’t trust testing it against 100% of real traffic. With the VirtualService from the previous section, they expose v2 to only 10% of traffic, watch the error metrics in Prometheus, and if everything looks good, gradually raise the weight up to 100%. If something fails, they drop the weight to 0 in seconds, with no deployment rollback.
stateDiagram-v2
[*] --> V1_100
V1_100 --> V1_90_V2_10 : start canary
V1_90_V2_10 --> V1_50_V2_50 : metrics ok
V1_50_V2_50 --> V2_100 : full promotion
V1_90_V2_10 --> V1_100 : rollback due to error rate
V1_50_V2_50 --> V1_100 : rollback due to error rate
Common mistakes and gotchas
- Startup order: the Envoy sidecar must be ready before the app starts making network calls, because the
iptablesrules are already active. Istio solves this with an init-container, but with a poorly configuredreadinessProbethe pod can end up inCrashLoopBackOffduring the first few seconds. - Memory and CPU usage: each sidecar is one more process per pod. In a cluster with hundreds of pods, that translates into a non-trivial infrastructure cost that needs to be factored in before deciding to adopt a mesh.
- Added latency: each request now passes through two extra proxies (the client’s and the server’s) before reaching its destination. You need to measure it with
istioctl proxy-configbefore assuming it doesn’t matter. - Unlabeled namespaces: it’s easy to forget to label a new namespace with
istio-injection=enabledand end up with services left outside the mesh without anyone noticing until strict mTLS fails. - Confusing the service mesh with an API Gateway: the mesh manages east-west traffic (between internal services); an API Gateway manages north-south traffic (between the external client and the cluster). They’re complementary pieces, not substitutes.
Comparison with alternatives
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| Istio + Envoy | Large clusters that need mTLS, canary releases, and full observability | More mature ecosystem, granular control via CRDs | Steep learning curve, one sidecar per pod |
| Linkerd | Teams that want mTLS and basic metrics without much configuration | Own proxy written in Rust, much lighter than Envoy | Fewer advanced routing features than Istio |
| Consul Connect | Companies that already use Consul for service discovery | Integrates with non-Kubernetes infrastructure (VMs, bare metal) | Less rich observability ecosystem than Istio |
| Cilium (sidecarless mode) | Clusters where the overhead of one sidecar per pod is unacceptable | Uses eBPF in the kernel, no extra container per pod | Requires a modern Linux kernel and has less maturity in layer 7 mTLS |
Going deeper: xDS, Ambient Mesh, and the sidecarless future
The protocol that connects the control plane to each Envoy is called xDS (discovery service), and it’s actually a family of APIs: LDS for listeners, RDS for routes, CDS for clusters (groups of endpoints), and EDS for individual endpoints. Every time Istiod detects a change (a new pod, an edited VirtualService), it pushes the update to all affected Envoys via gRPC streaming, without the proxy having to poll.
flowchart TD
A["kubectl apply VirtualService"] --> B["Istiod (control plane)"]
B -- "xDS via gRPC" --> C["Envoy sidecar Pod A"]
B -- "xDS via gRPC" --> D["Envoy sidecar Pod B"]
subgraph DataPlane
C
D
end
The cost of the sidecar (one container per pod, with its own memory footprint) has been the most persistent criticism of service meshes since they’ve existed. The most recent response from the Istio community is Ambient Mesh, a mode that takes the L7 proxy out of the pod and moves it to a proxy shared per node, reserving the sidecar only for cases that need mTLS or advanced L7 policies. Cilium, for its part, solves the same problem from a different angle: instead of a userspace proxy, it uses eBPF programs that run directly in the Linux kernel to apply network policies without the overhead of an extra process per pod.
Neither approach eliminates the complexity of the problem, it just relocates it: you trade one more container per pod for relying on a proxy shared per node, or for depending on a specific kernel version for your eBPF programs. Choosing between sidecar, ambient, or eBPF remains, in 2026, a trade-off decision and not a one-size-fits-all answer.
💭 Key takeaway: a service mesh doesn’t replace the need to design your microservices well: it moves network complexity out of the code, but operational complexity (certificates, versions, control plane upgrades) is still there, it’s just now handled by the platform team instead of each product team.
📖 Summary on Telegram: View summary
Your next step: install Istio on a local cluster with minikube or kind, label a test namespace, and run istioctl proxy-status to see your first sidecar synced with the control plane.
Frequently Asked Questions
Does a service mesh replace an API Gateway?
No. The service mesh manages east-west traffic between internal microservices; the API Gateway manages north-south traffic between external clients and the cluster. Many architectures use both at once, and Istio can actually act as an ingress gateway in addition to being an internal mesh.
Do I need Kubernetes to use a service mesh?
Not strictly: Consul Connect, for example, also works with virtual machines and bare metal. But the vast majority of Istio and Linkerd deployments assume Kubernetes as the base platform.
How much latency does an Envoy sidecar add?
It depends on the load and configuration, but the right way to find out in your case is to measure with istioctl proxy-config and compare latency percentiles with and without the sidecar active, instead of assuming a generic number.
What happens if the control plane (Istiod) goes down?
The sidecars keep working with the last configuration they received: xDS isn’t a synchronous dependency for every request. What’s temporarily lost is the ability to apply new routing or security changes until Istiod comes back.
Is a service mesh worth it for a small project?
Rarely. If you have fewer than a dozen services and don’t need mandatory mTLS or automated canary releases, the operational overhead of maintaining Istiod and the sidecars usually outweighs the benefit.
References
- Istio: official project documentation and installation guides with istioctl.
- Envoy Proxy: official site of the proxy that forms the data plane of Istio and other meshes.
- Kubernetes: Services, Load Balancing, and Networking: official documentation on Kubernetes networking.
- Linkerd: official documentation for the lightweight alternative to Istio.
- github.com/istio/istio: source repository of the Istio project.
📱 Do you like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Imagen destacada: Foto de Winston Chen en Unsplash
0 Comments