⏱️ 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
  1. TL;DR
  2. What is a service mesh and why it matters
  3. How it works: the sidecar pattern
  4. Practical examples: installing Istio and seeing the sidecar in action
  5. Circuit breakers and retries: resilience without extra code
  6. Observability: metrics, traces, and logs without instrumenting code
  7. Getting started: from zero to your first mesh on Kubernetes
  8. Real-world use cases
  9. Common mistakes and gotchas
  10. Comparison with alternatives
  11. Going deeper: xDS, Ambient Mesh, and the sidecarless future
  12. Frequently Asked Questions
    1. Does a service mesh replace an API Gateway?
    2. Do I need Kubernetes to use a service mesh?
    3. How much latency does an Envoy sidecar add?
    4. What happens if the control plane (Istiod) goes down?
    5. Is a service mesh worth it for a small project?
  13. 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 VirtualService to 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 outlierDetection without 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.

Architecture of a service mesh with Envoy sidecar proxies in Kubernetes
Each pod adds an Envoy container: the sidecar intercepts all network traffic. Foto de Cash Vickers en Unsplash

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:

  1. Install Istio on a staging cluster with the demo profile, as in the block above.
  2. Label only a test namespace with istio-injection=enabled, never the whole cluster at once.
  3. Redeploy the pods in that namespace (kubectl rollout restart deployment) so they receive the sidecar: injection only happens when the pod is created.
  4. Verify that the sidecar is running with kubectl get pod <name> -o jsonpath='{.spec.containers[*].name}': you should see istio-proxy alongside your app’s container.
  5. Confirm that the sidecar is synced with the control plane using istioctl proxy-status: the SYNCED column should say SYNCED for every proxy, not STALE.
  6. Enable mTLS in PERMISSIVE mode first (it accepts both encrypted and unencrypted traffic at once) and only move to STRICT afterward, 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.
Terminal showing the sync status of Envoy proxies with istioctl
istioctl proxy-status is the first command to run when something isn’t routing. Foto de Daniela Paola Alchapar en Unsplash

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-api example.
  • 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 iptables rules are already active. Istio solves this with an init-container, but with a poorly configured readinessProbe the pod can end up in CrashLoopBackOff during 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-config before assuming it doesn’t matter.
  • Unlabeled namespaces: it’s easy to forget to label a new namespace with istio-injection=enabled and 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

OptionWhen to use itAdvantageLimitation
Istio + EnvoyLarge clusters that need mTLS, canary releases, and full observabilityMore mature ecosystem, granular control via CRDsSteep learning curve, one sidecar per pod
LinkerdTeams that want mTLS and basic metrics without much configurationOwn proxy written in Rust, much lighter than EnvoyFewer advanced routing features than Istio
Consul ConnectCompanies that already use Consul for service discoveryIntegrates 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 unacceptableUses eBPF in the kernel, no extra container per podRequires 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

📱 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


Javier Alarcón

Infrastructure engineer specializing in networking, Linux systems, Kubernetes, and cloud architectures. Covers hardware, networking, observability, and engineering practices for production teams.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.