Sidecar pattern
Learn how the sidecar pattern decouples cross-cutting concerns from your services, how Envoy intercepts traffic transparently, and when to use it over shared libraries.
TL;DR
- The sidecar pattern deploys an auxiliary container alongside your main app in the same Kubernetes pod, handling cross-cutting concerns like logging, tracing, TLS, and retries without touching application code.
- Both containers share a network namespace (localhost) and any mounted volumes. The sidecar intercepts all inbound and outbound traffic using iptables rules, making the proxy transparent to the application.
- The core trade-off is operational leverage vs. resource overhead: one sidecar image managed by your platform team eliminates infrastructure drift across services, at the cost of ~50MB RAM and ~1-3ms latency per pod.
- Without sidecars, polyglot teams re-implement logging, tracing, and mTLS independently in each language, and those implementations drift apart over time. With sidecars, those concerns are owned by the platform team and updated without app code changes.
- The break-even: 3 or more microservices in 2 or more languages. Below that, a shared library is simpler. Above that, shared libraries collapse under governance debt.
The Problem
You're running 18 microservices across Go, Python, and Java. Six teams own those services. Your security team mandates mTLS between all services by Q3. Your observability team wants distributed tracing added everywhere.
Six teams spend six weeks integrating their respective TLS implementations. Three teams mishandle certificate rotation. Two teams ship different versions of the OpenTelemetry SDK. One team's Java service develops a memory leak in the trace exporter. The Go team finishes first, ships to production, and is already a version behind by the time the Python team ships.
Six months later: 18 services, 3 languages, and 18 slightly different implementations of the same 4 infrastructure concerns. Every security audit finds a different CVE in a different version. Every postmortem fights over whose trace was incomplete. Every sprint includes "keep library X up to date across all services" as a recurring ticket nobody wants to own.
The mistake I see most often is teams treating this as a documentation problem. "We'll write an internal guide and require all teams to follow it." That works for two teams for three months. With ten teams and five languages, it's a governance problem that no amount of code review can fix. The implementation diverges because the incentives diverge.
One-Line Definition
A sidecar co-deploys an auxiliary container with the main application in the same pod, sharing its network and filesystem to transparently handle cross-cutting infrastructure concerns without application code changes.
Analogy
Consider the classic motorcycle sidecar. The motorcycle handles propulsion, steering, and navigation. That is the core function. The sidecar is physically attached and travels everywhere with the motorcycle, but it carries things the motorcycle cannot handle alone: a passenger, luggage, extra cargo.
The motorcycle does not care what is in the sidecar. The sidecar does not control where the motorcycle goes. They share the same journey (the pod), each doing their own job completely.
Your app container is the motorcycle. The sidecar is the attached compartment handling observability, security, and networking. The motorcycle stays focused on getting somewhere. The sidecar handles everything else.
Solution Walkthrough
The shared network namespace
When Kubernetes schedules a pod, all containers in that pod share a single network namespace. This is the key mechanism. Every container in the pod communicates with every other container via localhost.
The sidecar does not need complicated routing to intercept traffic. It is already on the same loopback interface as the main app. An inbound request arrives at the sidecar's listening port, the sidecar processes it (TLS termination, tracing headers, retry policy), and forwards it to localhost:8080 where the app is listening.
From the app's point of view, there is no proxy. Just incoming requests on localhost. The app makes outbound calls normally; the kernel's iptables rules silently redirect them through Envoy before they leave the pod.
Traffic interception via iptables (Istio model)
In a service mesh like Istio, the sidecar does not just listen on a specific port. It intercepts all outbound and inbound traffic using iptables NAT rules injected at pod startup via an init container.
The init container runs before both the app and Envoy. It writes rules that redirect all outbound TCP from the pod (except traffic from UID 1337, which is Envoy itself) to Envoy's outbound port 15001. Inbound traffic is redirected to port 15006. The --uid-owner 1337 exemption is critical: without it, Envoy's outbound traffic would be redirected back to itself in an infinite loop.
The application makes normal socket calls. The kernel's network stack silently reroutes every connection through Envoy. This is the "transparent proxy" model.
For your interview: describe the pod as a unit where app and sidecar share localhost. Name Envoy and Istio. Mention the iptables interception if the interviewer asks how it works. That chain shows you understand the mechanism, not just the abstraction.
Implementation Sketch
Two concrete examples: Docker Compose for local development, Kubernetes for production.
# docker-compose.yml β SKETCH
# Illustrates structural relationship. In production, Envoy config is managed
# by the control plane (istiod), not a local file.
services:
app:
image: my-order-service:latest
expose:
- "8080" # App listens on 8080; NOT exposed externally
volumes:
- logs:/var/log/app # Shared volume with filebeat sidecar
envoy: # Sidecar 1: networking
image: envoyproxy/envoy:v1.29-latest
ports:
- "80:15001" # External traffic enters via Envoy, not the app
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
depends_on:
- app
filebeat: # Sidecar 2: log shipping
image: elastic/filebeat:8.12.0
volumes:
- logs:/var/log/app:ro # Read-only access to the same log volume
- ./filebeat.yaml:/usr/share/filebeat/filebeat.yml
volumes:
logs:
In Kubernetes, both sidecars become containers inside a single Pod spec:
# kubernetes/order-service-pod.yaml β SKETCH
apiVersion: v1
kind: Pod
metadata:
name: order-service
spec:
# Kubernetes 1.28+: initContainers with restartPolicy: Always are "native sidecars"
# They start before (and stop after) regular containers β solving the lifecycle race.
initContainers:
- name: istio-proxy
image: docker.io/istio/proxyv2:1.20.0
restartPolicy: Always # <-- K8s 1.28 native sidecar declaration
args: ["proxy", "sidecar"]
ports:
- containerPort: 15001 # outbound traffic redirect target
- containerPort: 15006 # inbound traffic redirect target
securityContext:
runAsUser: 1337 # UID exempted from iptables redirect
containers:
- name: order-service
image: my-order-service:latest
ports:
- containerPort: 8080
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
- name: filebeat
image: elastic/filebeat:8.12.0
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
readOnly: true
volumes:
- name: shared-logs
emptyDir: {}
Pre-K8s 1.28: sidecars have no lifecycle guarantee
Before Kubernetes 1.28, sidecars were just regular containers with no guaranteed startup or shutdown ordering. A common production bug: Filebeat exits before the app finishes flushing logs, losing the last N seconds of data on pod shutdown. The workaround is a preStop: exec: ["/bin/sleep", "5"] lifecycle hook on the Filebeat container, which delays its response to SIGTERM long enough to flush remaining log entries before exiting. K8s 1.28 native sidecars (initContainers with restartPolicy: Always) solve this cleanly: they are guaranteed to start before and stop after regular containers.
When It Shines
Ok, but here's the thing most people miss: the sidecar pattern is not a default for any microservices setup. It earns its overhead at a specific scale threshold.
Polyglot teams. The moment you have services in more than one language, a shared library strategy breaks. A Go library and a Python library for mTLS are two separate codebases that drift apart. A sidecar intercepts TCP, not function calls. Language-agnostic by design.
Platform engineering teams. When your company has a dedicated platform team, a sidecar is how that team delivers capabilities without code-level integration. The platform team ships one updated image; product teams get the upgrade with a version tag bump and zero code changes.
Security mandates fleet-wide. mTLS between 50 services is infeasible to implement in application code. One Envoy sidecar config pushed fleet-wide handles it. Each pod gets a SPIFFE identity (spiffe://cluster.local/ns/<ns>/sa/<sa>) embedded in its TLS certificate, issued by istiod's built-in CA β the sidecar makes mTLS zero-trust identity, not just encryption.
Consistent distributed tracing. For traces to span service boundaries, the traceparent (W3C standard) header must be forwarded consistently through every hop. Envoy does this automatically at the proxy level. App-level implementations miss headers, use wrong keys, or forget to forward entirely.
The rule of thumb: 3 or more microservices in 2 or more languages and you probably need this. A single monolith or two services in the same language and you almost certainly do not.
Failure Modes & Pitfalls
1. The startup race: app receives traffic before Envoy is ready
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.
Related Articles
Learn how the circuit breaker pattern stops cascading failures by failing fast on broken dependencies, and how three states protect your system.
Learn how the bulkhead pattern isolates resource pools to contain failuresβso one slow dependency can never exhaust your thread pool and take down every unrelated feature.
Learn how the Outbox pattern eliminates the dual-write problem in distributed systems, guaranteeing every database write produces its corresponding event even when brokers and services crash mid-flight.