In 2025–2026 the service mesh went through a paradigm shift: Istio moved everyone to ambient, Cilium spliced itself into the traffic path via eBPF, and the word “sidecar” became an insult in architecture discussions. Linkerd never dropped the sidecar — and never died: the 2.17 stable branch delivered federated services, egress metrics and rate limiting, while edge releases keep shipping almost weekly in 2026. The honest question: when is a megabyte-scale Rust proxy in the pod still the right answer, and when is it time to go ambient.
Why now: 2.17-stable closed the three big complaints about Linkerd — “multicluster means a separate extension”, “egress is a black hole”, “there is no rate limiting” — and Buoyant moved the project to a sustainability model, which the industry has spent two years misreading as “Istio won”. While everyone buries it, it keeps releasing.
Table of contents
Open Table of contents
Three generations of mesh, briefly
The first generation was a shared proxy per host: Linkerd 1.x, a heavy JVM monster, died first. The second was a sidecar in every pod: Istio put Envoy next to the app, Linkerd 2 shipped its own proxy rewritten from Scala to Rust precisely for the weight. The third is “sidecar-less”: Istio ambient (ztunnel per node plus waypoint for L7 — covered separately) and Cilium (eBPF in the kernel plus Envoy per node — also covered).
The main thing ambient made obvious: “sidecar-less” does not mean “no proxy”. The proxy just moves — out of the pod onto the node, and for L7 policies also into a waypoint next to the service. The argument was never “proxy or not”, it was “where do you pay”: RAM in every pod, or an agent on every node.
What arrived in 2.17
Not a revolution — three old gaps closed, plus a new release model:
- Federated services. A service is described once (
FederatedService) and lives in several clusters at the same time: one name, traffic goes to the nearest healthy endpoint, and when a cluster dies it fails over to the neighbor automatically. It evolves the multicluster extension and its mirrored services: fewer moving parts, one manifest. - Egress metrics. Outbound traffic leaving the cluster stopped being a black hole: external destinations now expose metrics and timings just like internal services. Previously this was a blind spot — what the prod talks to and how much was known only from app logs.
- Rate limiting in HTTPRoute. A local token bucket right in the proxy, with no external rate-limit service: the limit is part of the route rule and lives in the same manifest. The API is experimental, but it is the first native rate limiting in the project’s history.
- Edge vs stable. Buoyant moved development to a sustainability model: edge releases ship frequently (the
buoyant/linkerd2-edgerepo — practically weekly in 2026), stable releases rarely, with long support. Production runs stable only; edge is for touching features before the stable branch lands.
One corollary of this model: the project is financed through Buoyant support, not through fashion. “Linkerd is dead” is the most durable myth of the last two years.
Footprint: megabytes of Rust vs per-node agents
The three data plane layouts of 2026:
- Envoy sidecar (classic Istio): typically 100–200 MB of RAM per pod, plus noticeable CPU under load.
- Linkerd-proxy (Rust): ~10–20 MB RSS per pod, sub-millisecond added p99 latency, an image of a few dozen megabytes.
- ztunnel (ambient): tens of MB per node, amortized across all pods on the node — but it is pure L4. L7 policies require a waypoint, which is essentially the same proxy wearing a “sidecar of the service” badge instead of a “sidecar of the pod” one.
Arithmetic for a node with 30 pods: classic Istio — 30 × 150 MB ≈ 4.5 GB on sidecars alone; Linkerd — ~450 MB for the same pods; ambient — tens of MB per node, but every service that needs L7 policies adds a waypoint. The conclusion is not “who eats less” but where you are willing to pay: Linkerd — slightly more RAM for the simplicity of a pod → pod model and proxy isolation inside the pod; ambient — less RAM at the cost of an extra network hop, a second entity in your diagnostics, and a waypoint matrix for L7.
On small clusters — tens of nodes, tens of services — absolute numbers are small for everyone, and the choice is driven not by RAM but by how many entities you can explain to the on-call engineer.
mTLS and policy without a zoo
mTLS is on by default for every meshed pod: each pod gets a certificate carrying its ServiceAccount identity, rotation is automatic, and there is one trust root per cluster (or one shared across several — for federated services).
Policy is built around identity, not IP addresses. The Server CRD describes what you protect (pods plus a port), ServerAuthorization — who is allowed in. Until a Server exists, meshed pods talk to each other over mTLS without a single line of configuration; the moment a Server claims a port, all traffic without an mTLS identity is cut off. The default “mesh → mesh allowed, everything else denied” covers most real policies, and it does not need a matrix of AuthorizationPolicy objects per service pair, which is how it tends to go in Istio.
For HTTP-level rules — path, method, headers — there is a first-party HTTPRoute from policy.linkerd.io targeting a Server, and in 2.17 an AuthorizationPolicy on top of routes. But that is an option, not a mandatory layer.
Comparison in one table
| Linkerd 2.17 | Istio Ambient | Cilium mesh | |
|---|---|---|---|
| Data plane | Rust proxy in every pod | ztunnel per node + waypoint for L7 | eBPF in the kernel + Envoy per node |
| L7 policy | HTTPRoute (policy.linkerd.io) | HTTPRoute via waypoint | Gateway API + CiliumNetworkPolicy |
| RAM (node, 30 pods) | ~450 MB in sidecars | tens of MB/node + waypoint per service | nearly 0 for eBPF + Envoy per node |
| mTLS | out of the box, identity = ServiceAccount | out of the box in ztunnel | via SPIRE/cert-manager — enable and configure |
| Multicluster | federated services (2.17) | east-west gateway | clustermesh |
| Rate limiting | local, in HTTPRoute (2.17) | on waypoint/Envoy | Envoy configs |
| Entry barrier | one evening | weeks | in between, depends on appetite for living in the kernel |
The fork is simple: maximum functionality and heterogeneous policies — ambient; if the cluster already runs Cilium and it is your only CNI — its mesh is nearly free; minimal weight and the simplest story to tell — Linkerd.
What you need to run Linkerd on a test cluster
Prerequisites: a test cluster (kind/k3s), Helm 3, the step tool for generating test identity certificates (production uses cert-manager or a corporate PKI), and a demo app of one Deployment plus one Service.
1. Certificates and install. Linkerd lands as two Helm charts plus the viz extension for metrics:
step certificate create root.linkerd.cluster.local ca.crt ca.key \
--profile root-ca --no-password --insecure
step certificate create identity.linkerd.cluster.local issuer.crt issuer.key \
--profile intermediate-ca --no-password --insecure \
--ca ca.crt --ca-key ca.key
helm repo add linkerd https://helm.linkerd.io/stable && helm repo update
helm install linkerd-crds linkerd/linkerd-crds \
--namespace linkerd --create-namespace --wait
helm install linkerd-control-plane linkerd/linkerd-control-plane \
--namespace linkerd \
--set-file identityTrustAnchorsPEM=ca.crt \
--set-file identity.issuer.tls.crtPEM=issuer.crt \
--set-file identity.issuer.tls.keyPEM=issuer.key
helm install linkerd-viz linkerd/linkerd-viz \
--namespace linkerd-viz --create-namespace
2. The “before” measurement. Deploy the app as usual and capture its consumption:
kubectl -n demo apply -f app.yaml
kubectl -n demo top pods
3. Mesh. Injection is one namespace annotation and a restart:
kubectl annotate ns demo linkerd.io/inject=enabled
kubectl -n demo rollout restart deploy/web
A linkerd-proxy container appears next to every app container. Re-run kubectl -n demo top pods — that is your “after”: a typical delta of 10–20 MB RSS and negligible CPU per pod.
4. Policy. A Server plus a ServerAuthorization for one service:
apiVersion: policy.linkerd.io/v1beta1
kind: Server
metadata:
name: web
namespace: demo
spec:
podSelector:
matchLabels: { app: web }
port: http
---
apiVersion: policy.linkerd.io/v1beta1
kind: ServerAuthorization
metadata:
name: web
namespace: demo
spec:
server:
name: web
client:
meshUsers:
- demo.gateway # namespace.serviceaccount
That is it: the web port is open only to meshed clients with the demo/gateway identity.
5. Rate limit (2.17 experiment). The limit lives in an HTTPRoute rule targeting the same Server — the syntax is experimental, check the docs for your version:
apiVersion: policy.linkerd.io/v1alpha1
kind: HTTPRoute
metadata:
name: web-route
namespace: demo
spec:
targetRefs:
- { kind: Server, name: web }
rules:
- matches:
- { path: { type: PathPrefix, value: / } }
filters:
- type: RateLimitFilter # local token bucket in the proxy
rateLimit:
type: Local
local:
tokenBucket: { maxTokens: 20, tokensPerFill: 10, fillInterval: 1s }
How to verify it works
linkerd check
linkerd viz stat deploy -n demo
linkerd viz edges deploy -n demo
check— green across the board, data plane included.statshows the success rate, RPS and p50/p95 for every service — the gold people usually install a mesh for.edgesreportsTRUEin the SECURITY column: the connection is over mTLS.- The negative test — a pod outside the mesh running the same curl must be refused, because the Server does not authorize anonymous clients:
kubectl run probe --rm -it --image=curlimages/curl --restart=Never -- \
curl -sS http://web.demo.svc.cluster.local:8080/
If it still answers from outside the mesh, the Server is not selecting the port (check port: http — the port name in the Deployment must match).
Pitfalls
- Non-HTTP protocols. The proxy detects HTTP/2 and gRPC; database and broker traffic must be marked
config.linkerd.io/opaque-ports: "5432,9092"— it flows as opaque TCP with mTLS, without any attempt to parse the protocol. - Proxy CPU. Nearly zero at idle, suddenly needed at peak: keep requests around 100m and do not set a tight limit.
- Edge in production. Stable only: edge releases are “touch it early”, not “run it”.
- Rollback. Removing the mesh is removing the annotation and restarting — the app never knew Linkerd existed, the code never changed. This is the most underrated property of the proxy-in-pod approach: switch it on, switch it off.
Bottom line
Ambient did not kill Linkerd — it retired the sidecar argument. While the choice was “Envoy at 150 MB in every pod” or “nothing”, sidecar-less looked like the only option; now the choice is honest: a per-node agent with ztunnel and waypoint, or a megabyte-scale Rust proxy with a pod → pod model. For small and medium clusters Linkerd remains the cheapest way to get mTLS, per-service metrics and a sane policy model in one evening, and 2.17 closed the multicluster and egress gaps. The price is a sidecar in every pod and a bet on a quieter project. Hundreds of nodes with strict L7 policies everywhere — look at ambient or Cilium; tens of nodes — first count what exactly you are saving.