A service mesh injects a proxy into every pod to intercept all traffic between services, without changing a line of application code. What that proxy provides for free (mTLS, retries, L7 observability) has a real cost in operational complexity, and that cost differs radically between Istio and Linkerd.

What a service mesh solves that Kubernetes doesn’t

A Kubernetes Service routes traffic to healthy pods, but says nothing about what’s inside that traffic: no automatic encryption between pods, no retry on transient failure, no visibility into HTTP status codes or per-request latency. A NetworkPolicy filters who can talk to whom, but at the IP and port level, never at the application layer. A service mesh closes that gap by intercepting every call through a sidecar proxy, with zero code changes: mutual TLS encryption between every pod in the mesh, latency and error-rate metrics per service pair, retries and timeouts configured outside application code.

# Sidecar auto-injected into every pod in the namespace,
# application code never sees this proxy
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    istio-injection: enabled

Istio: maximum features, maximum surface

Istio builds its data plane on Envoy proxies, a general-purpose proxy extremely rich in features: weighted traffic splitting for canary releases, fault injection to test resilience, fine-grained per-rule authorization. That same Envoy consumes non-trivial CPU and memory per pod, and the control plane (istiod) becomes itself one more critical component to monitor, upgrade, and size.

# Istio VirtualService: 90% of traffic to v1, 10% to v2,
# a primitive Kubernetes alone doesn't offer natively
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: my-app
spec:
  hosts:
    - my-app
  http:
    - route:
        - destination: {host: my-app, subset: v1}
          weight: 90
        - destination: {host: my-app, subset: v2}
          weight: 10

The newer ambient mode removes the per-pod sidecar in favor of a shared per-node proxy (ztunnel), reducing overhead in exchange for weaker per-pod isolation: a trade-off to evaluate based on the trust level between workloads sharing a node.

Linkerd: the strict minimum, in Rust

Linkerd makes the opposite choice: a micro-proxy written in Rust (linkerd2-proxy), purpose-built for this exact role rather than repurposed from a general-purpose proxy, with a noticeably lighter memory and CPU footprint than Envoy. mTLS is on by default at injection time, no explicit configuration to write: the inverse of Istio, which exposes more levers but enables nothing by default.

# mTLS active right at injection, no extra
# resource to write to get it
linkerd inject deployment.yaml | kubectl apply -f -

The trade-off is a narrower feature set: no equally fine-grained weighted traffic splitting, no fault injection, no built-in API gateway. For a team that wants mTLS and observability without operating an extra control plane, that simplicity is the point; for a team that needs sophisticated routing rules, it’s a real limitation.

The real cost: operational debt, not licensing

Both projects are open source and free to use: the cost that actually decides in practice is operational time. Every sidecar adds latency (a few milliseconds per hop, but cumulative across a call chain spanning several services), consumes resources that scale with pod count, and introduces one more component to upgrade at every Kubernetes version bump. A poorly sized or never-upgraded mesh becomes silent debt, invisible until the incident that reveals a sidecar running an outdated version, incompatible with the rest of the cluster.

Takeaway

A service mesh delivers mTLS and L7 observability without touching application code, a real win for a multi-service system. Istio maximizes features (weighted routing, fault injection) at the cost of a heavier control plane to operate; Linkerd maximizes simplicity and lightness, at the cost of a narrower feature set. The deciding criterion is neither the license nor the project name, but the team’s actual capacity to operate one more component over the long run, the same kind of trade-off that gets decided from the start of a CI/CD industrialization.