Engineering Atlas › System Design Fundamentals

Helper processes with the sidecar pattern

11 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Co-location

In Kubernetes, a pod holds multiple containers that share a network namespace (the same localhost and IP) and can share volumes. The sidecar runs as one of those containers. On VMs, it is a second process managed by the same supervisor. Co-location gives the sidecar cheap, local access to the app: a loopback TCP call costs tens of microseconds, and a shared volume lets a log shipper read files the app writes.

Common roles

Library versus sidecar

The alternative is a shared library in every service. A library has no extra hop and no extra container, but must exist for every language, and upgrading it requires rebuilding and redeploying every service. A sidecar is language-agnostic and can be upgraded by the platform team on its own schedule, at the cost of resources and an extra process to fail. Microsoft's pattern guide adds two cases where a sidecar is the wrong call: when the component must scale independently of the app (make it a separate service), and when the platform or a sidecarless mesh already provides the capability (Azure Architecture Center).

Lifecycle

Ordering matters. If the app starts before the proxy sidecar is ready, its first outbound calls fail. If the sidecar exits first during shutdown, in-flight requests break. Kubernetes added native sidecar support (init containers with restartPolicy: Always) so sidecars start before and stop after the main container. It arrived in v1.28, has been on by default since v1.29, and is stable since v1.33 (Kubernetes docs); older guides that tell you to script around the ordering problem predate it. Jobs are a special case: a sidecar that never exits keeps a batch pod running forever unless it is a native sidecar, which does not block the Job from completing (Kubernetes docs).

Down the stack

Layer 1The contract

A proxy sidecar promises transparency: the application talks plain HTTP or TCP to the address it always used, and the sidecar adds encryption, policy, and telemetry without the app noticing. It also promises lifecycle ordering when deployed as a native sidecar: it is running before the app container starts and keeps running until the app has exited. A telemetry sidecar promises at-least-once shipping of what the app writes to a shared volume, with buffering if the backend is slow.

Layer 2The mechanism

Transparent interception is done with iptables in the pod's network namespace. In Istio, an init container or CNI plugin installs NAT rules: inbound TCP is redirected to the proxy on port 15006, outbound TCP to port 15001, and packets from the proxy's own user ID (1337) are excluded so the proxy's outbound connections do not loop back into itself. That is why Istio tells you never to run an application as UID 1337 (Istio docs). The proxy recovers the original destination address with the SO_ORIGINAL_DST socket option, which the kernel sets on connections redirected by an iptables REDIRECT (or TPROXY) rule, and routes accordingly (Envoy docs). Kubernetes native sidecars are init containers with restartPolicy: Always: the kubelet starts them in order before regular containers, waits for their startup probe, and terminates them after the main containers have fully stopped, in the reverse of their declared order. Shared volumes such as an emptyDir (optionally medium: Memory, a tmpfs) let a log shipper read the app's files.

Layer 3The machine

Every intercepted request crosses the kernel networking stack extra times: app to proxy over loopback, then proxy to the real destination, and the reverse on the receiving side. Each loopback hop costs syscalls, two copies, context switches, and conntrack lookups, roughly 10 to 50 microseconds, plus the proxy's own parsing and TLS work. Istio's own benchmark puts a sidecar Envoy at about 0.20 vCPU and 60 MB per 1,000 requests per second (Istio performance), but memory is mostly configuration: by default every sidecar is programmed to reach every workload in the mesh, so an Envoy holding routes and endpoints for hundreds of services can use far more unless a Sidecar resource scopes it to the services the pod actually calls (Google Cloud docs). Multiplied by thousands of pods, that is the dominant cost, which is why meshes offer per-namespace scoping and node-level alternatives.

Why it's built this way

  1. Platform teams cannot modify every application in every language, so capabilities are attached from outside the process.
  2. Every Linux network namespace already supports packet redirection, so iptables can intercept without app cooperation.
  3. Redirection hides the original destination, so the proxy reads it back from the socket, and excludes its own UID to avoid loops.
  4. A second process in the same pod has its own failure and startup timing, so Kubernetes added explicit sidecar lifecycle ordering.

Diagram

flowchart LR
    subgraph POD["Pod (shared network namespace)"]
        APP["App container"]
        PX["Proxy sidecar"]
        LOG["Log shipper sidecar"]
        VOL[("Shared volume")]
    end
    IN["Inbound traffic"] -->|"mTLS"| PX
    PX -->|"localhost"| APP
    APP -->|"outbound via localhost"| PX
    PX -->|"mTLS, retries, timeouts"| UP["Upstream services"]
    APP -->|"writes logs"| VOL
    VOL --> LOG
    LOG --> BACK["Log backend"]

Worked example

A company runs 1,200 pods across 150 services in four languages. They want mTLS between all services, uniform retry policies, and per-route metrics.

With a proxy sidecar per pod: each Envoy uses about 50 MB of memory and 0.05 vCPU at idle, rising to 0.2 vCPU under load. Across 1,200 pods that is 60 GB of memory and 60 to 240 vCPU, roughly 8% of cluster capacity. Each request crosses two proxies (client side and server side), adding about 0.5 to 1 ms at p50 and 2 to 3 ms at p99.

Against that, the platform team rolled out mTLS in three weeks without touching any service code, and the language-specific retry libraries (four implementations, all slightly different) were deleted. When a CVE hit the proxy, they upgraded all 1,200 sidecars in a day with a rolling restart, without asking 40 teams to rebuild.

They also hit the startup race: about 0.3% of pods failed their first database call because the app started before the proxy. Switching to native sidecars fixed it.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Shared libraryNo extra hop or process, lowest overheadOne implementation per language, upgrades need redeploysOne or two languages, tight latency budgets
Sidecar per instanceLanguage-agnostic, independent upgrades, strong isolationPer-pod CPU and memory, extra hop, lifecycle issuesPolyglot fleets, platform-managed capabilities
Per-node agent (DaemonSet)One copy per node, lower total resourcesShared failure domain per node, weaker tenant isolationLog and metric collection, node-level networking
Sidecarless mesh (eBPF or node proxies)Lower overhead, no pod injection; Istio ambient is GA since 1.24 (Istio)Shared per-node L4 proxy, L7 needs separate waypointsLarge fleets where sidecar cost dominates

Pitfalls in production

Leadership lens

Check yourself

  1. Why can a sidecar be upgraded without rebuilding the application?

    Show answer

    It is a separate container image with its own release cycle, and it interacts with the app only through local network calls or shared files.

  2. What problem do Kubernetes native sidecars solve?

    Show answer

    They guarantee the sidecar starts before and stops after the main container, and they do not keep batch jobs alive after the main container exits.

  3. When is a per-node agent better than a per-pod sidecar?

    Show answer

    For capabilities like log or metric collection where per-workload isolation is not needed and running one copy per node saves substantial resources.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →