Downward API
The Downward API exposes a curated subset of Pod and Container metadata from the Kubernetes API into the container as environment variables or files, so application code can read facts about itself — its own Pod name, the namespace it runs in, the node hosting it, its CPU/memory limits — without needing API-server credentials. The Kubernetes documentation defines the use case: “It is sometimes useful for a container to have information about itself, without being overly coupled to Kubernetes. The downward API allows containers to consume information about themselves or the cluster without using the Kubernetes client or API server.” (k8s.io — Downward API). The name “downward” reflects the data-flow direction: information flows down from the Pod spec into the running container, not up from the container into the API. The two delivery modes — environment variables and a special
downwardAPIvolume — mirror those of ConfigMap and Secret, with the same update-semantics asymmetry: env vars are frozen at container start (with one notable exception introduced for in-place resource resize), while volume-mounted files are live-updated by the kubelet. The Downward API is deliberately narrow in what it exposes: only the fields explicitly listed in the spec, not arbitrary fields of the Pod object. This is a security design choice — preventing a container from reading the Pod’s full spec (which might include other containers’ Secrets, image-pull credentials, sensitive annotations) by accident.
Mental Model
flowchart LR subgraph SPEC["Pod object (in API server)"] META["metadata:<br/> name: my-app-0<br/> namespace: payments<br/> uid: 7f...<br/> labels: { app: my-app }<br/> annotations: { team: data }"] STATUS["status:<br/> podIP: 10.244.3.15<br/> hostIP: 10.0.5.4"] RES["spec.containers[0].<br/>resources:<br/> requests: { cpu: 500m }<br/> limits: { memory: 1Gi }"] NODE["spec.nodeName: ip-10-0-5-4"] SA["spec.serviceAccountName: payments-sa"] end subgraph PROJ["kubelet projection layer"] FR["fieldRef"] RFR["resourceFieldRef"] end subgraph CONTAINER["Container"] ENV["env:<br/>POD_NAME=my-app-0<br/>POD_NAMESPACE=payments<br/>NODE_NAME=ip-10-0-5-4<br/>CPU_REQUEST=500m"] VOL["downwardAPI volume<br/>/etc/podinfo/labels<br/>/etc/podinfo/cpu_limit"] end META --> FR STATUS --> FR NODE --> FR SA --> FR RES --> RFR FR --> ENV RFR --> ENV FR --> VOL RFR --> VOL
What this diagram shows. Two API “selectors” — fieldRef for object-level metadata and resourceFieldRef for container-level resource fields — pick fields out of the Pod object and inject them into the container as either env vars or files. The diagram makes visible the field-by-field nature of the API: you do not get the whole Pod spec, you get only the keys you explicitly named. The insight to extract: the Downward API is a curated, schema-validated projection of the Pod object, designed so application code can be “K8s-aware without being K8s-clienty” — useful for clustered apps that need to register peers by Pod name, for JVMs that need their memory limit to size the heap, and for log shippers that need to tag events with the namespace and node, all without granting the Pod any RBAC permission on the K8s API.
Mechanical Walk-through
What fields are exposable
The official list, distilled from k8s.io — Downward API:
Via fieldRef (Pod-level fields):
Available through both env vars AND downwardAPI volume:
metadata.name— the Pod’s namemetadata.namespace— the namespacemetadata.uid— the Pod’s UID (immutable per-Pod-object)metadata.labels['<key>']— value of a specific labelmetadata.annotations['<key>']— value of a specific annotation
Available through env vars only:
spec.serviceAccountName— Pod’s ServiceAccountspec.nodeName— node the Pod is scheduled onstatus.hostIP— primary IP of the nodestatus.hostIPs— dual-stack version ofstatus.hostIPstatus.podIP— Pod’s primary IP (typically IPv4)status.podIPs— dual-stack version ofstatus.podIP
Available through downwardAPI volume only:
metadata.labels— all labels, formatted askey="value"one per line in the projected filemetadata.annotations— all annotations, same format
Via resourceFieldRef (Container-level resource fields):
requests.cpu,requests.memorylimits.cpu,limits.memoryrequests.ephemeral-storage,limits.ephemeral-storagerequests.hugepages-<pagesize>,limits.hugepages-<pagesize>
The crucial absences:
- No
spec.containers[*]— a container cannot read the spec of its sibling containers. - No
spec.volumes, noimagePullSecrets— credentials and storage references are deliberately hidden. - No
statusbeyond IPs — nostatus.conditions, nostatus.containerStatusesself-introspection. - No
metadata.ownerReferences— you can’t trivially discover your parent Deployment/StatefulSet.
These omissions are intentional: the field list is the security boundary. To get anything more, the Pod’s ServiceAccount needs real RBAC permission on the K8s API and the application has to use a Kubernetes client.
The two delivery modes
Mode 1: env vars
containers:
- name: app
image: my-app:v1
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: MEMORY_LIMIT_BYTES
valueFrom:
resourceFieldRef:
containerName: app # required: which container's resources to read
resource: limits.memory
divisor: 1 # bytes; could also be 1Mi for mebibytesThe kubelet evaluates these expressions when constructing the container’s environment at start. They are frozen for the container’s lifetime under the classical semantics — once the process is running, the env vars don’t change.
Exception (K8s 1.35+): in-place Pod resource resize (the ability to change a running container’s CPU/memory requests without restarting) was promoted in 1.35. The K8s docs note that env-var resourceFieldRef values do not update when resources are resized — only the volume-mounted equivalents update dynamically. This is the same general principle (env vars are frozen) but now visibly limiting in a feature that is designed to mutate at runtime.
Mode 2: downwardAPI volume
containers:
- name: app
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: labels # file at /etc/podinfo/labels
fieldRef:
fieldPath: metadata.labels # all labels, one per line
- path: annotations
fieldRef:
fieldPath: metadata.annotations
- path: cpu_limit
resourceFieldRef:
containerName: app
resource: limits.cpu
divisor: 1m # millicores
- path: pod_name
fieldRef:
fieldPath: metadata.nameEach item creates a file at <mountPath>/<path> containing the projected value. Key behaviors:
- The whole
metadata.labelsandmetadata.annotationsare only exposable via the volume, formatted as<key>="<escaped-value>"lines. - The mount is tmpfs, like Secrets — the data never touches the node’s disk.
- Live updates: when the Pod’s labels or annotations are edited (e.g.,
kubectl label pod ...), or when its resources are resized in-place, the kubelet re-projects the affected files within the kubelet sync period. - Atomic projection: updates use the same rename-into-place pattern as ConfigMap volume mounts; the application never reads a torn write.
The divisor field for resource subdivision
When exposing CPU and memory, the raw representation might not be what your application wants. The divisor field controls the unit:
- name: CPU_LIMIT_MILLICORES
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.cpu
divisor: 1m # value reported as N millicores (e.g., "2000")
- name: CPU_LIMIT_CORES
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.cpu
divisor: 1 # value reported as N cores (e.g., "2", rounded UP if fractional)
- name: MEM_LIMIT_MIB
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.memory
divisor: 1Mi # value reported as N mebibytesThe kubelet rounds the resource value UP to the nearest multiple of the divisor. Pick the divisor that matches your application’s tuning needs — JVMs typically want 1Mi for memory and 1 for CPU; a thread-pool sizing routine might want 1m for fractional-CPU precision.
The “default to node allocatable” rule
If you reference limits.cpu or limits.memory for a container that doesn’t actually set those limits, the kubelet defaults to the node’s allocatable capacity rather than zero or an error. The K8s docs note: “If CPU and memory limits are not specified for a container, and you use the downward API to try to expose that information, then the kubelet defaults to exposing the maximum allocatable value for CPU and memory based on the node allocatable calculation.” This makes the field still useful for sizing decisions, but watch out for the inflation — a JVM that reads MEM_LIMIT from the Downward API and sizes its heap to 80% of that will use 80% of the node’s memory if no limit was set, OOM-killing every other Pod.
Configuration / API Surface
Canonical example: a clustered app self-registering by Pod name
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
spec:
serviceName: kafka
replicas: 3
selector:
matchLabels: { app: kafka }
template:
metadata:
labels: { app: kafka }
spec:
containers:
- name: kafka
image: kafka:3.7
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name # kafka-0, kafka-1, kafka-2
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: KAFKA_BROKER_ID
value: $(POD_NAME) # uses K8s env-var-in-env-var substitution
- name: KAFKA_ADVERTISED_LISTENERS
value: PLAINTEXT://$(POD_NAME).kafka.$(POD_NAMESPACE).svc.cluster.local:9092This is the canonical StatefulSet + Downward API + Headless Service pattern: Kafka brokers know their own stable identity (kafka-N), and the headless service plus <pod-name>.<svc>.<ns>.svc.cluster.local DNS gives them a stable resolvable hostname.
Canonical example: JVM heap sizing from container limits
containers:
- name: jvm-app
image: openjdk:17
resources:
requests: { memory: 1Gi, cpu: 500m }
limits: { memory: 1Gi, cpu: 1 }
env:
- name: MEM_LIMIT_MIB
valueFrom:
resourceFieldRef:
containerName: jvm-app
resource: limits.memory
divisor: 1Mi
command: ["sh", "-c"]
args:
- |
java -Xmx$(( MEM_LIMIT_MIB * 75 / 100 ))m \
-XX:MaxRAMPercentage=75 \
-jar /app.jarModern JVMs (17+) can do -XX:MaxRAMPercentage natively, reading the cgroup memory limit themselves — but pre-cgroup-v2 environments or non-JVM languages without that auto-detection still benefit from the explicit Downward API plumbing.
Canonical example: log shipper tagging events with Pod metadata
- name: fluent-bit
image: fluent/fluent-bit
env:
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
- name: POD_NAMESPACE
valueFrom: { fieldRef: { fieldPath: metadata.namespace } }
- name: NODE_NAME
valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
# Fluent Bit's output configuration interpolates these into structured log fieldsdownwardAPI volume with all labels
volumes:
- name: podinfo
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels # ALL labels, only available via volume
- path: annotations
fieldRef:
fieldPath: metadata.annotationsRead by an application like:
cat /etc/podinfo/labels
# app="payments"
# version="v1.4.2"
# env="prod"This is the right pattern when the application needs to react to label changes at runtime (e.g., a service mesh sidecar that re-routes based on a label edit).
Failure Modes
- Reference to a field that isn’t in the exposable list. Pod creation fails admission with
field not implementedor similar. There is no “expose this random field” escape valve — the list is the list. metadata.labelsreferenced in an env var. Not allowed (env var requires a single value; labels is a map). Use volume mode instead.resourceFieldReffor a container with nolimits.memoryset. Silently returns the node’s allocatable memory, often much larger than intended. Symptom: JVM heap auto-sized to the whole node, OOM-killing siblings. Mitigation: always setlimitsexplicitly, or document the dependence.- In-place resource resize, app reads env var. The env var stays at the old value; the volume file updates. Application that relies on
MEM_LIMIT_MIBenv var won’t see the new size. Mitigation: use volume mode if your application participates in in-place resize. downwardAPIvolume mounted withsubPath. Same trap as ConfigMap volumes:subPathdisables live updates. Use the directory mount pattern.- Trying to read another container’s resources.
resourceFieldRefrequirescontainerName, and the kubelet validates that the named container exists in the Pod, but you can read across siblings — useful for sidecars sizing themselves relative to the main container. This is not a failure mode but worth knowing.
Alternatives and When to Choose Them
- Direct K8s API access via the Pod’s ServiceAccount token. When you need more than the Downward API exposes (e.g., owner reference walking, sibling Pod discovery, cross-namespace reads), use the in-cluster K8s client with explicit RBAC. The cost is a real K8s client dependency and a real RBAC posture.
- ConfigMap for static cluster-wide info. Cluster name, region, AZ-to-rack mapping — these don’t change per Pod and belong in a ConfigMap, not the Downward API.
- Projected Volume combining Downward API + ConfigMap + Secret + ServiceAccount token. When you want one mount path holding everything the container needs (e.g.,
/etc/app-infocontaining both Pod metadata and a TLS cert), use a projected volume. This is the modern pattern that supersedes single-sourcedownwardAPIvolumes for complex setups. - JVM/runtime cgroup-aware sizing. Modern JVMs (11+ with
-XX:+UseContainerSupport, on by default in 17+), Node.js, .NET 7+, and Go (1.19+) read their container limits directly from cgroups. For these, the Downward API is redundant for resource limits — but still useful for non-resource fields like Pod name and namespace. - Init container that calls the K8s API to write to a shared volume. A heavier alternative when you need richer info than Downward API exposes; the init container uses a service-account token to query the API and writes results to an
emptyDirfor the main container to read. Use sparingly — adds complexity and RBAC surface.
Production Notes
- The “no client, no RBAC” property is the real win. Production K8s shops universally prefer Downward API over direct K8s API access for simple introspection because: (a) it adds no client-library dependency to the app, (b) it requires no RBAC on the Pod’s ServiceAccount, (c) it’s stable across K8s minor versions (the field list rarely changes), (d) it’s available to dead-simple languages like shell scripts.
- The Spotify “every Pod knows its node” pattern. Most production observability stacks tag every log line and metric with
pod_name,pod_namespace,node_name,pod_ip— all from the Downward API. Spotify’s, Shopify’s, and Airbnb’s published infrastructure blogs all show variations of this; the Downward API plumbing is part of every shared “base container” or “platform sidecar” template. - Cluster-wide identity: pair with
ServiceAccountprojected tokens. For Pods that need cloud-IAM identity (e.g., S3 access), use workload identity via projected ServiceAccount tokens, not the Downward API. The Downward API only knows what’s in the Pod spec; cloud IAM identity is delivered via the OIDC-federated token path, which is a different (Projected Volume-based) mechanism. - Annotations as a coordination channel. A pattern: a controller annotates the Pod with state-of-the-world info (
infra.example.com/leader: "true"), the Pod’s Downward API volume re-projects the annotation, and a sidecar in the Pod reads it from/etc/podinfo/annotationsto update its behavior. This avoids requiring the sidecar to have K8s API access. Limit: kubelet sync period (~1 minute), so reaction time is slow. - In-place resize gotcha (1.35+). As in-place Pod resource resize becomes more common, expect a wave of “my app didn’t notice the new CPU limit” reports because the env-var path doesn’t propagate. The fix is volume-mount the resource limit if your application reacts to changes; otherwise document that env-var resource-limit consumers see the initial values only.
See Also
- Pod — the source object whose fields are projected
- ConfigMap — sibling for external config; nearly identical consumption surface
- Secret — sibling for sensitive config
- Projected Volume — modern pattern combining Downward API, ConfigMap, Secret, ServiceAccount token
- Volume Types (Kubernetes) —
downwardAPIis one of the documented volume types - ServiceAccount — sibling Pod-introspection mechanism for K8s API access
- StatefulSet — canonical consumer (Pod name → stable peer identity)
- Resource Requests and Limits — the source of
resourceFieldRefvalues - Kubernetes Labels and Selectors —
metadata.labelsis exposable; bulk-only via volume - Workload Identity (Kubernetes) — adjacent pattern for cloud-IAM identity, via projected tokens not Downward API
- Kubernetes MOC §8 — Configuration and Secrets