The problem
Every service needs plumbing that has nothing to do with its business logic: encrypting and retrying its calls, shipping its logs, fetching its secrets and settings, exporting its telemetry. There are two obvious places to put that plumbing, and both hurt.
Inside the application, as a library. It is fast and sees everything in the process, but it has to be written again for every language and framework, it reaches production only when each service is rebuilt and redeployed, and it shares the application's fate: a memory leak in the log shipper is now a memory leak in the checkout service. With several languages and dozens of teams, nobody can say which services already run last month's fix.
In a separate service, somewhere on the network. It is independent, but it is far away: one more network call, one more thing to discover, authenticate to and scale, and it cannot read the application's local files or reach its loopback interface.
The helper should be as close to the application as a library (same host, same network identity, same files, same lifetime) and as separate from it as a service (own code base, own release cycle, own resource limits).
How it works
A sidecar is a helper process deployed next to each instance of the application, inside the same deployment unit. In Kubernetes that unit is the pod; elsewhere it is an Amazon ECS task, a Nomad task group, a Cloud Run instance or simply the same host. The name comes from the motorcycle sidecar: bolted to the bike, going wherever it goes, pointless without it. The Azure Architecture Center also calls it the sidekick pattern.
Because the two run side by side, they can cooperate without any shared code:
- Over
localhost. The containers share one network namespace, so the application reaches the helper (and the helper reaches the application) on a loopback port, in plain HTTP or gRPC, whatever language either is written in. - Through files. A volume mounted into both lets one write what the other reads: the application appends to a log file and the helper ships it, or the helper writes a configuration file and the application reads it.
- By sitting in the path. A proxy sidecar receives the application's outbound and inbound connections and adds what the application does not implement.
The application's own code carries none of the helper's logic, and usually does not know the helper exists.
Three sibling patterns, and where the names come from
The vocabulary comes from the people who built Kubernetes. Brendan Burns described three ways of composing containers on one node in a 2015 Kubernetes blog post, written after a DockerCon talk, and again with David Oppenheimer in a 2016 HotCloud paper:
| Pattern | What the helper does | Example |
|---|---|---|
| Sidecar | In Burns's words, sidecars "extend and enhance" the main container | a log shipper, a process that keeps local files in sync with a repository |
| Ambassador | Proxies the application's connections to the outside world, so the application only ever talks to localhost |
a proxy that adds TLS and retries, or one that spreads requests over the shards of a cache |
| Adapter | Presents the application to the outside world in a standard form | an exporter that turns an application's own metrics into the format the monitoring system expects |
Today sidecar usually names the deployment mechanism, and ambassador and adapter name what a particular helper does: an ambassador is deployed as a sidecar. In the animation the proxy is an ambassador that also takes inbound calls, and the log shipper is the paper's own first example.
The paper also gives the reasons for keeping the helper in a container of its own: a container is the unit of resource accounting, of packaging and team ownership, of reuse, of failure containment and of deployment. It names the price of the last one too: the helper and the application are upgraded separately, so every combination of versions that can meet in production has to work.
What is shared and what is isolated
| Shared by the pod | Separate for each container | |
|---|---|---|
| Network | one network namespace: the same IP address, the same port space, the same localhost (so two containers cannot listen on the same port) |
|
| Storage | only the volumes that are mounted into both | the root file system, which comes from each container's own image |
| Lifecycle | scheduled, started, scaled and deleted as one unit | a crashed container is restarted on its own; each has its own probes |
| Resources | the scheduler places the pod by the sum of its containers' requests | each container has its own CPU and memory requests and limits |
| Processes | optional: with shareProcessNamespace: true every container sees the others' processes |
the default: separate process namespaces |
| Identity | the pod's service account, whose token is mounted into every container by default |
The isolation is what a library cannot give you: a sidecar that exceeds its memory limit is killed and restarted without taking the application's process with it, and its CPU limit keeps it from starving the request path.
Lifecycle and ordering
"Started and stopped together" hides three details that cause most of the trouble with sidecars:
- At startup the helper has to be working before the application needs it. An application that starts faster than its proxy has no network for its first calls.
- At shutdown the helper has to outlive the application, or the last requests cannot leave and the last log lines are never shipped.
- A job that finishes must not be kept alive by a helper that never exits.
For years Kubernetes had no notion of a sidecar. A helper was just another entry in containers: nothing made the application wait for it, at shutdown the containers were signalled in no guaranteed order, and a Job's pod never completed while the helper kept running. Teams worked around it with postStart hooks that held the application back, preStop hooks that delayed the helper, and scripts that told the helper to quit when the job was done.
Native sidecar containers replace those workarounds. A sidecar is declared as an init container with restartPolicy: Always. The feature arrived as alpha in Kubernetes v1.28 (August 2023), has been on by default since v1.29 and has been stable since v1.33 (April 2025); every release still maintained in October 2026 (the newest is v1.37) has it, and it can no longer be switched off. It changes four things:
- Start. Init containers start in the order they are listed. The kubelet moves on to the next one, and finally to the application containers, once the sidecar counts as started: when its
startupProbesucceeds, or as soon as its process runs if it has none. Regular init containers listed after a sidecar can already use it. - Run. The sidecar is restarted whenever it exits, whatever the pod's own restart policy says. It can have startup, readiness and liveness probes, and its readiness counts towards the pod's.
- Stop. The kubelet sends the termination signal to the sidecars only after the last application container has fully stopped, and stops them in reverse order. If the grace period runs out first, everything that is left is killed together, so a non-zero exit code from a sidecar at shutdown is normal.
- Jobs. A sidecar does not keep a Job's pod from completing.
Other platforms have their own form of the same idea. An Amazon ECS task definition marks containers as essential and orders them with dependsOn conditions (START, COMPLETE, SUCCESS, HEALTHY), applied in reverse at shutdown. A Nomad task with a lifecycle block and sidecar = true runs for as long as the main tasks do and is stopped after them. Cloud Run starts the containers of an instance in a declared order and can wait for each one's health check.
Update and reuse independently
Because the helper is a separate image, the team that owns it can release it on its own schedule, and the same image serves every language. That is the third step of the animation: one proxy image next to a Java, a Go and a Python service, where a library would need three implementations kept equal and a rebuild of every service for every fix.
"Without rebuilding" does not mean "without restarting". With the usual tooling (an injector, or a workload whose pod template changes) a new sidecar version reaches a workload when its pods are replaced: a rolling restart with the application image untouched. Until every workload has been restarted, the fleet runs a mix of sidecar versions.
When to use it
- A capability that many services need in the same way, in more than one language: transport security, retries, log shipping, secret delivery, telemetry export.
- A helper owned by another team (platform, security, observability) that should ship on its own schedule.
- An application you cannot or do not want to change: legacy code, a third-party image, a product without an extension mechanism.
- A helper that has to be local to the instance because it needs the application's files, its loopback interface or its identity.
- A helper that should have its own resource limits and should be able to fail without taking the application down.
Library, sidecar, node agent or platform?
The same capability can live in four places:
| Library | Sidecar | One agent per node | Platform feature | |
|---|---|---|---|---|
| Runs | inside the application's process | next to each instance, in the same pod | once on every node | in the platform |
| Languages | one implementation per language | any | any | any |
| Sees | everything in the process | one instance's network, mounted files and identity | every workload on the node | what the platform exposes |
| Cost grows with | the application itself | the number of instances | the number of nodes | nothing you operate |
| A new version needs | a rebuild of every service | a restart of every pod | a rollout of the agent | a platform upgrade |
| A failure affects | the application's process | the helper of one instance | every workload on that node | everyone on the platform |
When a library is better
- Performance-critical paths. Every call through a sidecar is serialised, crosses the loopback interface and is parsed again. For a chatty or latency-critical interface an in-process call wins.
- Deep integration. Some things need knowledge that only exists inside the process: which request is safe to retry, what the current user may do, where a span starts and ends in the code. Tracing is the usual example: the instrumentation is a library, and the Collector beside the application only receives and forwards what it produces (see Telemetry Pipeline).
- One language and one team. A dependency upgrade is simpler than another container in every pod.
When one agent per node is better
- Log collection at scale. If applications write to standard output, the node already has every container's log files, and one agent per node (a DaemonSet in Kubernetes) collects them for all pods without touching any of them. The Kubernetes logging guide warns that a logging agent in a sidecar can consume significant resources and that logs handled this way no longer show up in
kubectl logs. - Anything that is about the node rather than the application: host metrics, the kubelet, the container runtime.
- Cost. Twenty agents on twenty nodes instead of two hundred in two hundred pods.
The price of a per-node agent is sharing: it serves every tenant on the node, needs node-level privileges, cannot be configured per application as freely, and takes more workloads with it when it fails.
When the platform should do it
If the platform already provides the capability, a sidecar only adds parts. Kubernetes mounts Secrets and ConfigMaps as files and refreshes them after a change (not for subPath mounts), the node keeps every container's standard output as log files, a CSI driver can mount secrets from an external store, and service meshes offer data planes without sidecars: in Istio's ambient mode a proxy on each node replaces the one in each pod (see Service Mesh).
When not to use it
- The helper has to scale differently from the application, or is shared by many applications. That is a separate service.
- The application is small or runs in few instances. The overhead per instance and the extra moving parts can outweigh the isolation.
- The interface between the two is chatty or latency-critical. Use a library.
- The platform or a per-node agent already does the job.
- To split one application in two. A sidecar is for a helper with the same lifetime as the application. Two pieces of business logic that happen to talk a lot are either one service or two.
- Instances that must start fast, such as workloads that scale from zero: every start now waits for the sidecar as well.
Trade-offs
Resource overhead, and how to budget it
Every pod pays again. The animation uses example figures: a proxy with 0.1 CPU and 100 MB, in 200 pods, reserves 20 CPU and 20 GB before any application does any work. Real figures depend on the helper and the traffic: in Istio's own benchmark (version 1.24, 1,000 requests per second with 1 KB payloads) one proxy sidecar with two worker threads used about 0.20 vCPU and 60 MB.
- Give every sidecar explicit requests and limits. The requests of the sidecars are added to those of the application containers, and that sum is what the scheduler places and what a quota counts. Injectors have defaults and per-workload overrides, for example Istio's
sidecar.istio.io/proxyCPUandsidecar.istio.io/proxyMemoryannotations. - Mind the quality-of-service class. A pod is
Guaranteedonly if every container, sidecars included, has CPU and memory limits equal to its requests. One injected container without them changes the class of the whole pod. - Mind the autoscaler. A HorizontalPodAutoscaler that targets CPU utilisation computes it over all containers of the pod, and takes no action for that metric if any container lacks a CPU request. A
ContainerResourcemetric (stable since Kubernetes v1.30) scales on the application container alone. Autoscaling covers the rest. - Or budget the pod as a whole. Pod-level resources (beta since v1.34, on by default) set one CPU and memory budget that the containers share.
One more hop
A proxy sidecar adds a hop to every call, and two when both ends have one. Istio's documentation puts the latency its sidecar mode adds, with a proxy at each end, at 0.63 ms (p90) to 0.88 ms (p99). That is nothing for one call and noticeable for a chain of ten. Measure your own tail latency with the sidecar in place rather than trusting anyone's benchmark.
Security: the sidecar is inside the trust boundary
- It sees what the pod sees. It shares the network namespace, so it can connect to anything the application binds to
localhost, including admin and debug endpoints, and it can listen on the pod's ports. It reads every volume mounted into it. It runs with the pod's service account. - So a compromised sidecar is a compromised pod, and the other way round. Traffic between the application and a proxy sidecar is plaintext on the loopback interface, and a secret that an agent writes to a shared volume is readable by every container that mounts it. The pattern moves code out of the application, not out of its blast radius.
- Injection is a supply chain. Sidecars are often added by a mutating admission webhook when the pod is created. Whoever controls that webhook, its configuration or the sidecar image runs code in every pod. Pin image versions, restrict who can change the injector, and review what it adds.
- Privileges. A proxy that intercepts traffic transparently has to rewrite the pod's packet routing. Istio's init container needs the
NET_ADMINandNET_RAWcapabilities for that, unless its CNI node agent does the work instead. Do not give a sidecar more privileges than the application has. - Mount little. Give the sidecar only the volumes it needs, read-only where it only reads.
On the positive side, a sidecar can add a security control such as Mutual TLS to an application that has none, without changing it.
Debugging two processes
- Ask which container. Logs, restarts and exit codes are per container (
kubectl logs <pod> -c <container>). A502or503may come from the proxy and not from the application. - Readiness is shared. A pod can be running and still not ready because the sidecar's readiness probe fails, and an application can be healthy but unreachable because its proxy has crashed and is being restarted.
- Read the symptom as an ordering problem first. Refused connections in the first seconds mean the application started before the helper. Errors in the last seconds mean the helper stopped first. A Job that never completes means a helper that never exits.
- Look inside. An ephemeral debug container (
kubectl debug, stable since Kubernetes v1.25) can join a running pod and target one container's process namespace. WithshareProcessNamespace: truethe containers see each other's processes and, through/proc, each other's file systems, which is useful for debugging and one more reason to treat the pod as a single trust boundary. - Record the sidecar version next to the application version. After an upgrade of the injector, pods of the same workload can run different sidecar versions until they are all restarted.
Implementation notes
Declare it as a native sidecar in Kubernetes. The
restartPolicyon an init container is the whole difference:spec: initContainers: - name: log-shipper image: registry.example/logship:3.1 restartPolicy: Always # makes this init container a sidecar startupProbe: # the application starts once this succeeds httpGet: { path: /ready, port: 2020 } resources: requests: { cpu: 100m, memory: 100M } limits: { cpu: 100m, memory: 100M } volumeMounts: - { name: logs, mountPath: /var/log/app, readOnly: true } containers: - name: orders image: registry.example/orders:4.2 volumeMounts: - { name: logs, mountPath: /var/log/app } volumes: - name: logs emptyDir: {}Define "started" with a startup probe. Without one, the sidecar counts as started the moment its process runs, which is earlier than the moment it can serve.
Budget the shutdown. The grace period (30 seconds by default) covers the application and then the sidecars. An application that uses all of it leaves the sidecar no time to flush.
Keep the interface language-neutral: HTTP or gRPC on
localhost, files on a shared volume, or a Unix socket on a shared volume.Injection. Platforms usually add the sidecar with a mutating admission webhook, switched on by a label or annotation, so application teams do not maintain it in their manifests. Pods that already exist are not changed: the sidecar appears, and a new version arrives, when the pod is recreated (for example with
kubectl rollout restart). If an older tool or webhook in the admission chain drops fields it does not know, a native sidecar loses itsrestartPolicyand blocks the pod's startup.Updating in place. Kubernetes allows the image of a container in a running pod to be changed, and restarts only that container. Controllers such as Deployments still replace the pods when their template changes. OpenKruise's SidecarSet uses the in-place route to upgrade sidecar images without recreating pods.
Watch for collisions. An injected sidecar occupies ports and sometimes a user ID in the pod. Istio's proxy, for example, runs as UID 1337, which the application must not use.
Examples, verified in October 2026:
- A service mesh proxy. Istio injects Envoy as the
istio-proxycontainer into pods of namespaces labelledistio-injection=enabled, and Linkerd injects its Rustlinkerd-proxyinto workloads annotatedlinkerd.io/inject: enabled. Both use native sidecar containers by default, Istio since 1.27 and Linkerd since 2.20. What many such proxies do together under a control plane is the subject of Service Mesh. - An OpenTelemetry Collector as a sidecar. The OpenTelemetry Operator injects a Collector into pods annotated
sidecar.opentelemetry.io/inject, from anOpenTelemetryCollectorresource whosemodeissidecar. The same Collector also runs once per node (daemonset) or as a shared gateway; Telemetry Pipeline describes when to use which. - A secrets agent injector. HashiCorp's Vault Agent Injector is a mutating webhook that reacts to the annotation
vault.hashicorp.com/agent-inject: "true". It adds an init container that fetches the secrets before the application starts and a sidecar that keeps authenticating and rendering them, into an in-memory volume mounted at/vault/secrets. The application reads files and knows nothing about Vault. The alternative per node is the Secrets Store CSI Driver, a DaemonSet that mounts secrets from an external store as a volume. - An application runtime that works through a sidecar. Dapr (a CNCF graduated project) runs a
daprdprocess beside each application, injected into pods annotateddapr.io/enabled: "true". The application calls it onlocalhost, over HTTP (port 3500 by default) or gRPC (50001), for state management, publish and subscribe, service invocation, secrets and workflows. It is injected as a regular container by default and as a native sidecar on request (dapr.io/enable-native-sidecar: "true").