<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yosshi_</title>
    <description>The latest articles on DEV Community by yosshi_ (@yosshi_).</description>
    <link>https://dev.to/yosshi_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4167980%2F83ccb051-b6c0-43c0-b5db-451d1e502407.jpg</url>
      <title>DEV Community: yosshi_</title>
      <link>https://dev.to/yosshi_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yosshi_"/>
    <language>en</language>
    <item>
      <title>Inside kube-controller-manager: A Guide to Its 49 Controllers</title>
      <dc:creator>yosshi_</dc:creator>
      <pubDate>Wed, 21 Oct 2026 01:05:38 +0000</pubDate>
      <link>https://dev.to/yosshi_/inside-kube-controller-manager-a-guide-to-its-49-controllers-52n6</link>
      <guid>https://dev.to/yosshi_/inside-kube-controller-manager-a-guide-to-its-49-controllers-52n6</guid>
      <description>&lt;p&gt;A Deployment is not progressing, a Namespace is stuck terminating, or Pods remain on an unreachable Node. All three symptoms can involve kube-controller-manager, but they lead to different controllers, queues, and configuration settings.&lt;/p&gt;

&lt;p&gt;This guide maps the &lt;strong&gt;49 controllers registered in Kubernetes v1.37.1&lt;/strong&gt; to their responsibilities. It also explains why a registered controller might not start, how to inspect runtime configuration, and what to check before increasing concurrency or splitting the process.&lt;/p&gt;

&lt;p&gt;The controller inventory and binary defaults target &lt;strong&gt;v1.37.1&lt;/strong&gt;. The runtime output shown later was recorded with &lt;strong&gt;kindest/node:v1.37.0&lt;/strong&gt;. kubeadm and managed Kubernetes services can override the binary defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the objects to find the responsible controller
&lt;/h2&gt;

&lt;p&gt;kube-controller-manager runs many control loops in one process. A typical controller watches API objects through an informer, queues object keys when something changes, reads the cached state, and makes API requests to reconcile it with the desired state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmg4k5jo5vf9nhw6ptqha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmg4k5jo5vf9nhw6ptqha.png" alt="Official Kubernetes cluster architecture diagram showing kube-controller-manager in the control plane and its relationship to the API server and worker nodes" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://kubernetes.io/docs/concepts/architecture/" rel="noopener noreferrer"&gt;Kubernetes documentation — Cluster Architecture&lt;/a&gt;. The diagram places kube-controller-manager in the control plane; it coordinates changes through the API server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a Deployment rollout, the responsibilities form a chain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment controller&lt;/td&gt;
&lt;td&gt;Creates and scales ReplicaSets during a rollout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReplicaSet controller&lt;/td&gt;
&lt;td&gt;Creates or deletes Pods to maintain the requested replica count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kube-scheduler&lt;/td&gt;
&lt;td&gt;Assigns unscheduled Pods to Nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;Runs the Pods assigned to its Node through the container runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Deployment controller does not start containers directly. When investigating a rollout, identify the object where progress stopped: Deployment, ReplicaSet, Pod scheduling, or Pod execution.&lt;/p&gt;

&lt;p&gt;Multiple controllers can share an informer and its cache within a process. Their workqueues and reconciliation logic remain separate. Worker concurrency, API request throttling, and cache memory are therefore different bottlenecks.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kubernetes.io/docs/concepts/architecture/controller/" rel="noopener noreferrer"&gt;controller model&lt;/a&gt; describes the general pattern. The v1.37.1 &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/controller/deployment/deployment_controller.go" rel="noopener noreferrer"&gt;Deployment implementation&lt;/a&gt; provides a concrete example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registered does not mean running
&lt;/h2&gt;

&lt;p&gt;Controller names are stable identifiers defined in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/names/controller_names.go#L43-L93" rel="noopener noreferrer"&gt;controller_names.go&lt;/a&gt;. Each registered descriptor associates a name with a constructor, aliases, required feature gates, and whether it is disabled by default.&lt;/p&gt;

&lt;p&gt;Three checks determine whether a controller starts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Selection:&lt;/strong&gt; &lt;code&gt;--controllers&lt;/code&gt; must select it. &lt;code&gt;*&lt;/code&gt; means controllers enabled by default, rather than every registered controller. &lt;code&gt;-&amp;lt;name&amp;gt;&lt;/code&gt; excludes one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature gates:&lt;/strong&gt; every gate required by the descriptor must be enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construction:&lt;/strong&gt; controller-specific prerequisites must be satisfied. A constructor can decline to start a controller, for example when Node CIDR allocation is disabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;code&gt;--controllers=*&lt;/code&gt; and the default feature gates, &lt;strong&gt;43 of the 49&lt;/strong&gt; pass the first two checks. That is not a guarantee that 43 controllers are running: keys, certificates, API availability, and other settings still matter.&lt;/p&gt;

&lt;p&gt;The startup checks and registration list are in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/controller_descriptor.go" rel="noopener noreferrer"&gt;controller_descriptor.go&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three controllers are excluded from the default selection
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bootstrap-signer-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signs the bootstrap discovery &lt;code&gt;cluster-info&lt;/code&gt; ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token-cleaner-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes expired bootstrap token Secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;selinux-warning-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reports potential conflicts from Pods sharing a volume with different SELinux labels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;kubeadm explicitly adds the first two using their aliases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--controllers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;,bootstrapsigner,tokencleaner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kubeadm/app/phases/controlplane/manifests.go#L362" rel="noopener noreferrer"&gt;kubeadm configuration choice&lt;/a&gt;, not the kube-controller-manager binary's default. The SELinux warning controller can be added with &lt;code&gt;--controllers=*,selinux-warning-controller&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three more are blocked by default feature gates
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Required gates&lt;/th&gt;
&lt;th&gt;Default barrier in v1.37.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podgroup-protection-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GenericWorkload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Beta, disabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storageversion-garbage-collector-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;APIServerIdentity&lt;/code&gt;, &lt;code&gt;StorageVersionAPI&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;StorageVersionAPI&lt;/code&gt; is alpha and disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resourcepoolstatusrequest-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DRAResourcePoolStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alpha, disabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Adding a controller name does not override a disabled feature gate. Beta also does not necessarily mean enabled by default. The relevant gate definitions are in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/features/kube_features.go" rel="noopener noreferrer"&gt;Kubernetes features&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/apiserver/pkg/features/kube_features.go" rel="noopener noreferrer"&gt;apiserver features&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Other prerequisites include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Additional prerequisite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node IPAM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--allocate-node-cidrs=true&lt;/code&gt; and CIDR configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServiceAccount token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--service-account-private-key-file&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSR signing&lt;/td&gt;
&lt;td&gt;Shared or signer-specific CA certificate and private key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API server serving ClusterTrustBundle publisher&lt;/td&gt;
&lt;td&gt;A CA to publish and an available ClusterTrustBundle API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Garbage collector&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--enable-garbage-collector=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage version migrator&lt;/td&gt;
&lt;td&gt;Garbage collection enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The complete controller inventory
&lt;/h2&gt;

&lt;p&gt;The groups below are a way to navigate responsibilities, not categories defined by Kubernetes source code. Unless noted, a controller is included in the default selection and is not blocked by a default-disabled descriptor gate. Controller-specific prerequisites still apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workloads: 11 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manages ReplicaSets for Deployments and rollout progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replicaset-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintains the requested Pod count for ReplicaSets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replicationcontroller-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintains Pod counts for the legacy ReplicationController API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;statefulset-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manages StatefulSet Pod identities, ordering, and PVC creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemonset-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintains Pods on eligible Nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;job-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manages Job Pods, completions, failures, and retry behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cronjob-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates Jobs according to a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ttl-after-finished-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes finished Jobs after &lt;code&gt;ttlSecondsAfterFinished&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;horizontal-pod-autoscaler-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adjusts replica counts through the scale subresource using metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disruption-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Calculates PodDisruptionBudget status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podgroup-protection-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protects PodGroups in use; requires the default-disabled &lt;code&gt;GenericWorkload&lt;/code&gt; gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The disruption controller updates the budget; the API server's Eviction API uses that status when deciding whether to allow an eviction. Similarly, HPA reconciliation is built in, but its metric providers are separate components.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/apps.go" rel="noopener noreferrer"&gt;workload controller constructors&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/batch.go" rel="noopener noreferrer"&gt;batch controller constructors&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nodes: 4 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-lifecycle-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Monitors Node health, updates conditions, and applies health-related taints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taint-eviction-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes Pods that do not tolerate a Node's &lt;code&gt;NoExecute&lt;/code&gt; taints, including expired tolerations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-ipam-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allocates Pod CIDRs to Nodes when configured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ttl-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Annotates Nodes with a ConfigMap/Secret cache TTL hint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Node failure detection and Pod deletion are separate steps. Node lifecycle examines heartbeats from Node Leases and status updates; taint eviction acts on &lt;code&gt;NoExecute&lt;/code&gt; taints and Pod tolerations. The default &lt;code&gt;--node-monitor-grace-period&lt;/code&gt; is 50 seconds, but this alone does not define the time until a replacement Pod runs.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ttl-controller&lt;/code&gt; is unrelated to finished Job cleanup. Its &lt;code&gt;node.alpha.kubernetes.io/ttl&lt;/code&gt; annotation is used by the kubelet's TTL-based ConfigMap/Secret cache strategy. It does not set the default Watch strategy's update interval.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/core.go" rel="noopener noreferrer"&gt;core constructors&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/controller/ttl/ttl_controller.go" rel="noopener noreferrer"&gt;TTL implementation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage: 7 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentvolume-binder-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coordinates PV/PVC binding and provisioning-related lifecycle work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentvolume-attach-detach-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coordinates volume attachment to and detachment from Nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentvolume-expander-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coordinates expansion after a PVC's storage request increases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentvolume-protection-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protects PVs in use from deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentvolumeclaim-protection-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protects PVCs in use from deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volumeattributesclass-protection-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protects VolumeAttributesClasses in use from deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ephemeral-volume-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates PVCs for generic ephemeral volumes, owned by the Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Storage operations cross component boundaries. For CSI volumes, expansion typically also involves an external-resizer and the CSI driver; filesystem expansion can involve the kubelet and CSI driver. A stuck operation is not necessarily a bottleneck inside kube-controller-manager.&lt;/p&gt;

&lt;p&gt;The protection controllers use finalizers to defer deletion while the object remains in use. See &lt;a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storage-object-in-use-protection" rel="noopener noreferrer"&gt;persistent volume protection&lt;/a&gt; and &lt;a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims" rel="noopener noreferrer"&gt;volume expansion&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Services and networking: 4 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpoints-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builds Endpoints from Pods matching Service selectors; the Endpoints API is deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpointslice-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builds EndpointSlices from Pods matching Service selectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpointslice-mirroring-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mirrors manually managed Endpoints into EndpointSlices; also deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service-cidr-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintains ServiceCIDR status and deletion protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Service ClusterIP allocation itself belongs to the API server's allocator. The ServiceCIDR controller protects and maintains the range objects.&lt;/p&gt;

&lt;p&gt;Endpoints remains selected by default in this version despite API deprecation. For selectorless Services, prefer creating EndpointSlices directly rather than relying on Endpoints mirroring. See &lt;a href="https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/" rel="noopener noreferrer"&gt;EndpointSlices&lt;/a&gt; and the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/discovery.go" rel="noopener noreferrer"&gt;discovery controller constructors&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Namespace lifecycle and garbage collection: 5 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;namespace-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes namespaced content and completes Namespace termination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resourcequota-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recalculates ResourceQuota usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;garbage-collector-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes dependent objects according to ownership and deletion propagation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pod-garbage-collector-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes terminated Pods above the threshold and handles certain orphaned Pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storageversion-garbage-collector-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes obsolete StorageVersion objects; requires &lt;code&gt;StorageVersionAPI&lt;/code&gt; and &lt;code&gt;APIServerIdentity&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A terminating Namespace can be waiting on discovery, unavailable APIs, remaining objects, or their finalizers. More namespace workers cannot fix an unavailable API or a finalizer whose controller is absent.&lt;/p&gt;

&lt;p&gt;ResourceQuota accounting also has two sides: this controller updates usage, while quota admission enforcement happens in the API server. The Pod GC threshold defaults to &lt;strong&gt;12,500&lt;/strong&gt; terminated Pods; Pod GC is distinct from owner-reference garbage collection.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://kubernetes.io/docs/concepts/architecture/garbage-collection/" rel="noopener noreferrer"&gt;garbage collection&lt;/a&gt; and the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/core.go" rel="noopener noreferrer"&gt;core constructors&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security and certificates: 13 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serviceaccount-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates the default ServiceAccount in each Namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serviceaccount-token-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maintains Secret-based ServiceAccount tokens when a signing key is configured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;legacy-serviceaccount-token-cleaner-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cleans up eligible unused legacy Secret-based tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clusterrole-aggregation-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Aggregates rules for ClusterRoles with &lt;code&gt;aggregationRule&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root-ca-certificate-publisher-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Publishes the &lt;code&gt;kube-root-ca.crt&lt;/code&gt; ConfigMap in Namespaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;certificatesigningrequest-approving-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatically approves specific classes of CSR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;certificatesigningrequest-signing-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signs approved CSRs for configured built-in signers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;certificatesigningrequest-cleaner-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes old CSRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podcertificaterequest-cleaner-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes old PodCertificateRequests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube-apiserver-serving-clustertrustbundle-publisher-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Publishes the API server's serving CA as a ClusterTrustBundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bootstrap-signer-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signs bootstrap discovery information; excluded from &lt;code&gt;*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;token-cleaner-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes expired bootstrap tokens; excluded from &lt;code&gt;*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;validatingadmissionpolicy-status-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reports expression type-checking results in ValidatingAdmissionPolicy status&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not confuse the ServiceAccount token controller with projected, expiring Pod tokens. The latter are issued by the API server through TokenRequest and retrieved and refreshed by the kubelet.&lt;/p&gt;

&lt;p&gt;PodCertificateRequest cleanup is built in; certificate issuance requires a separate signer controller. CSR approval also does not mean arbitrary CSR requests are automatically approved.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/certificates.go" rel="noopener noreferrer"&gt;certificate controller constructors&lt;/a&gt;, &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/service_accounts.go" rel="noopener noreferrer"&gt;ServiceAccount constructors&lt;/a&gt;, and &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/" rel="noopener noreferrer"&gt;certificate API documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  API maintenance and resource allocation: 5 controllers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storage-version-migrator-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rewrites stored objects in response to StorageVersionMigration requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resourceclaim-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates and deletes ResourceClaims from Pod ResourceClaimTemplates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resourcepoolstatusrequest-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Processes ResourcePoolStatusRequests; requires default-disabled &lt;code&gt;DRAResourcePoolStatus&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device-taint-eviction-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Evicts Pods in response to DRA device taints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;selinux-warning-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Emits events and metrics about potential shared-volume SELinux conflicts; excluded from &lt;code&gt;*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The storage version migrator processes migration requests. A trigger that detects storage-version changes and creates those requests is a separate concern. The in-tree migration design leaves automatic triggering to users; see &lt;a href="https://github.com/kubernetes/enhancements/blob/8c2bd2ef10809249b4cec72145db9bb78430fb35/keps/sig-api-machinery/4192-svm-in-tree/README.md" rel="noopener noreferrer"&gt;KEP-4192&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What kube-controller-manager does not provide
&lt;/h2&gt;

&lt;p&gt;An API object can exist without an implementation that acts on it. These components are outside kube-controller-manager and must be provided by your distribution, platform, or installation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Separate component&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vertical Pod resource adjustment&lt;/td&gt;
&lt;td&gt;Vertical Pod Autoscaler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node autoscaling or provisioning&lt;/td&gt;
&lt;td&gt;Cluster Autoscaler or Karpenter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingress or Gateway traffic handling&lt;/td&gt;
&lt;td&gt;An Ingress controller or Gateway API implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster DNS&lt;/td&gt;
&lt;td&gt;CoreDNS or another cluster DNS implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External DNS records&lt;/td&gt;
&lt;td&gt;ExternalDNS or another DNS integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application certificate issuance and renewal&lt;/td&gt;
&lt;td&gt;cert-manager or another certificate controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PodCertificateRequest issuance&lt;/td&gt;
&lt;td&gt;A signer controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU/memory Metrics API&lt;/td&gt;
&lt;td&gt;Metrics Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics derived from Kubernetes object state&lt;/td&gt;
&lt;td&gt;kube-state-metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configurable custom-resource state metrics&lt;/td&gt;
&lt;td&gt;resource-state-metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic storage version migration requests&lt;/td&gt;
&lt;td&gt;A migration trigger, such as the separate kube-storage-version-migrator trigger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HPA is built in, while Metrics Server is not. Likewise, the presence of certificate cleanup controllers does not imply a complete application certificate issuance system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzv0w8avtglrsqt5i0xk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzv0w8avtglrsqt5i0xk.png" alt="Official Kubernetes components diagram distinguishing kube-controller-manager from cloud-controller-manager and cloud provider APIs" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://kubernetes.io/docs/concepts/overview/components/" rel="noopener noreferrer"&gt;Kubernetes documentation — Kubernetes Components&lt;/a&gt;. kube-controller-manager and cloud-controller-manager are separate control-plane components.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cloud-specific Node initialization, load balancer management, and route management belong to &lt;strong&gt;cloud-controller-manager&lt;/strong&gt;, not this 49-controller inventory. See the &lt;a href="https://kubernetes.io/docs/concepts/architecture/cloud-controller/" rel="noopener noreferrer"&gt;cloud controller architecture&lt;/a&gt; and &lt;a href="https://kubernetes.io/docs/concepts/overview/components/" rel="noopener noreferrer"&gt;Kubernetes component overview&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the process before changing its configuration
&lt;/h2&gt;

&lt;p&gt;The secure HTTPS port defaults to &lt;strong&gt;10257&lt;/strong&gt;. With appropriate network access and authentication/authorization, inspect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/flagz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect runtime flag values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/statusz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect binary version, start time, uptime, and related status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect controllers, queues, API clients, and process metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ComponentFlagz&lt;/code&gt; and &lt;code&gt;ComponentStatusz&lt;/code&gt; are beta and enabled by default in v1.37.1. The &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/controllermanager.go#L240-L250" rel="noopener noreferrer"&gt;endpoint registration&lt;/a&gt; is conditional on those gates. z-pages are intended for human debugging; their text output is not a stable parsing interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  A disposable kind inspection example
&lt;/h3&gt;

&lt;p&gt;The following lab configuration exposes the listener to the API proxy and bypasses endpoint authorization for the named debug paths. It is intended for this disposable kind cluster; it is not a production access configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# kind-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kind.x-k8s.io/v1alpha4&lt;/span&gt;
&lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;control-plane&lt;/span&gt;
  &lt;span class="na"&gt;kubeadmConfigPatches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;apiVersion: kubeadm.k8s.io/v1beta4&lt;/span&gt;
    &lt;span class="s"&gt;kind: ClusterConfiguration&lt;/span&gt;
    &lt;span class="s"&gt;controllerManager:&lt;/span&gt;
      &lt;span class="s"&gt;extraArgs:&lt;/span&gt;
      &lt;span class="s"&gt;- name: bind-address&lt;/span&gt;
        &lt;span class="s"&gt;value: "0.0.0.0"&lt;/span&gt;
      &lt;span class="s"&gt;- name: authorization-always-allow-paths&lt;/span&gt;
        &lt;span class="s"&gt;value: "/healthz,/livez,/readyz,/flagz,/statusz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; kcm-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; kind-config.yaml &lt;span class="nt"&gt;--image&lt;/span&gt; kindest/node:v1.37.0

&lt;span class="nv"&gt;POD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system get pods &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kube-controller-manager &lt;span class="nt"&gt;-o&lt;/span&gt; name | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;/ &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl get &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"/api/v1/namespaces/kube-system/pods/https:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:10257/proxy/flagz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An excerpt from the recorded v1.37.0 output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concurrent-deployment-syncs:5
concurrent-job-syncs:5
controllers:[*,bootstrapsigner,tokencleaner]
kube-api-burst:30
kube-api-qps:20
leader-elect:true
leader-elect-resource-name:kube-controller-manager
node-monitor-grace-period:50s
terminated-pod-gc-threshold:12500
use-service-account-credentials:true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;use-service-account-credentials:true&lt;/code&gt;: kubeadm supplies it, while the binary default is &lt;code&gt;false&lt;/code&gt;. This is why inspecting the running process matters.&lt;/p&gt;

&lt;p&gt;The same proxy path with &lt;code&gt;/statusz&lt;/code&gt; provides runtime status. Structured JSON can be requested through a client that sets the full content-negotiation header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Accept header for /flagz&lt;/span&gt;
&lt;span class="s1"&gt;'Accept: application/json;v=v1beta1;g=config.k8s.io;as=Flagz'&lt;/span&gt;

&lt;span class="c"&gt;# Accept header for /statusz&lt;/span&gt;
&lt;span class="s1"&gt;'Accept: application/json;v=v1beta1;g=config.k8s.io;as=Statusz'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structured request needs &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt;, and &lt;code&gt;as&lt;/code&gt;; an incomplete structured header receives &lt;code&gt;406 Not Acceptable&lt;/code&gt;. See &lt;a href="https://kubernetes.io/docs/reference/instrumentation/zpages/" rel="noopener noreferrer"&gt;Kubernetes z-pages&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Clean up the inspection cluster afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; kcm-inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Diagnose queues, API calls, and process resources separately
&lt;/h2&gt;

&lt;p&gt;Start with the affected controller and the objects it is trying to reconcile. The common metrics provide different kinds of evidence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What to investigate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;running_managed_controllers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which controllers are running, using &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;manager&lt;/code&gt; labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;leader_election_master_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether an instance is the leader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_depth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queued work awaiting processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_queue_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time spent waiting before processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_work_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time spent processing an item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_unfinished_work_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accumulated elapsed time of unfinished work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_longest_running_processor_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Longest-running current item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_retries_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate-limited requeues, which can include causes other than errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rest_client_request_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API request latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rest_client_rate_limiter_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time waiting for client-side request throttling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;process_resident_memory_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process resident memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Queue &lt;code&gt;name&lt;/code&gt; values are not guaranteed to match canonical controller IDs one-to-one. Inspect actual labels. Metric availability also depends on controller startup, registration, and whether the corresponding activity occurred. Definitions are in the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/component-base/metrics/prometheus/workqueue/metrics.go" rel="noopener noreferrer"&gt;workqueue metrics&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/component-base/metrics/prometheus/restclient/metrics.go" rel="noopener noreferrer"&gt;REST client metrics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For classic histograms, queue waiting-time p99 can be calculated with this PromQL. Select the intended scrape targets and retain labels such as cluster or instance when comparing independent queues:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;histogram_quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;sum&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workqueue_queue_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your scrape configuration stores this metric as a native histogram, use the base metric name and remove &lt;code&gt;le&lt;/code&gt; from the grouping. Check the actual stored representation before choosing the query.&lt;/p&gt;

&lt;p&gt;Controller-specific metrics help connect queue symptoms to outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Useful metric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;job_controller_job_sync_duration_seconds&lt;/code&gt;, &lt;code&gt;job_controller_job_syncs_total&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CronJob&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cronjob_controller_job_creation_skew_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;&lt;code&gt;horizontal_pod_autoscaler_controller_reconciliation_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node lifecycle&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;node_collector_zone_health&lt;/code&gt;, &lt;code&gt;node_collector_evictions_total&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taint eviction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;taint_eviction_controller_pod_deletions_total&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node IPAM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node_ipam_controller_cidrset_usage_cidrs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;node_collector_evictions_total&lt;/code&gt; counts application of eviction-related Node taints, not deleted Pods. &lt;code&gt;node_collector_zone_health&lt;/code&gt; is a percentage from 0 to 100, while CIDR set usage is a ratio from 0 to 1. Similar names do not imply the same unit or lifecycle stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tune the bottleneck you actually found
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Worker concurrency
&lt;/h3&gt;

&lt;p&gt;Increasing workers can help when independent objects are waiting and CPU and API capacity remain available. Representative binary defaults are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Controller&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-deployment-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReplicaSet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-replicaset-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StatefulSet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-statefulset-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-job-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CronJob&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-cron-job-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-daemonset-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disruption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-disruption-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-namespace-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Garbage collector&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--concurrent-gc-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More workers can also increase concurrent API requests and CPU use. If the API client is already throttled, worker count alone may not improve throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  API client rate limits
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;--kube-api-qps=20&lt;/code&gt; and &lt;code&gt;--kube-api-burst=30&lt;/code&gt; are defaults applied when constructing API clients. &lt;strong&gt;They are not a process-wide total limit of 20 requests per second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The normal client builder copies configuration to construct clientsets. Unless an explicit limiter is supplied, client-go creates a rate limiter during clientset construction. Independently constructed clients can therefore have separate limiters. Shared informer and discovery clients further complicate the total.&lt;/p&gt;

&lt;p&gt;Inspect client-side waiting and API server latency together. Raising QPS can remove a local bottleneck while increasing pressure on the API server and etcd. Separate ServiceAccount credentials improve identity and permission separation; they do not themselves remove throttling.&lt;/p&gt;

&lt;p&gt;See the tagged &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/controller-manager/pkg/clientbuilder/client_builder.go#L44-L66" rel="noopener noreferrer"&gt;client builder&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/client-go/kubernetes/clientset.go#L507-L519" rel="noopener noreferrer"&gt;clientset rate-limiter initialization&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batching, periodic work, and cache memory
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Tradeoff to consider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints and EndpointSlices&lt;/td&gt;
&lt;td&gt;Batching updates can reduce writes while delaying propagation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EndpointSlice size&lt;/td&gt;
&lt;td&gt;A larger &lt;code&gt;--max-endpoints-per-slice&lt;/code&gt; reduces object count but increases each object's size; default is 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;A longer sync period reduces evaluation frequency but slows response; default is 15 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ResourceQuota&lt;/td&gt;
&lt;td&gt;A longer periodic sync reduces full recalculation frequency; default is five minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod GC&lt;/td&gt;
&lt;td&gt;A lower terminated-Pod threshold removes historical objects earlier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node lifecycle&lt;/td&gt;
&lt;td&gt;Evaluate grace periods together with Lease/status reporting and tolerations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The default informer &lt;code&gt;--min-resync-period&lt;/code&gt; is 12 hours, randomized into a 12–24 hour range. Resync gives cached objects another processing opportunity; normal change detection uses Watch. Increasing this period does not directly reduce the number of objects retained in memory.&lt;/p&gt;

&lt;p&gt;Use the &lt;a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/" rel="noopener noreferrer"&gt;command reference&lt;/a&gt; to navigate flags, then confirm defaults against the relevant release and effective values through &lt;code&gt;/flagz&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More replicas provide availability, not more active workers
&lt;/h2&gt;

&lt;p&gt;With the same leader-election lock, one kube-controller-manager instance runs the controllers as leader. Adding identical replicas improves failover availability; it does not divide the reconciliation workload among them.&lt;/p&gt;

&lt;p&gt;There are two distinct approaches when tuning one process is insufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Split controller responsibilities across processes
&lt;/h3&gt;

&lt;p&gt;You can select different controller groups and give each group its own leader-election lock. These commands illustrate selection and lock names only; they omit the other controllers and required authentication and configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Workload group&lt;/span&gt;
kube-controller-manager &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--controllers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deployment-controller,replicaset-controller,job-controller,statefulset-controller &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--leader-elect-resource-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kcm-workload

&lt;span class="c"&gt;# Node group&lt;/span&gt;
kube-controller-manager &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--controllers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node-lifecycle-controller,node-ipam-controller,taint-eviction-controller &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--leader-elect-resource-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kcm-node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete design needs several decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assign every required controller to a group. Explicit lists leave unlisted controllers out. A remainder group using &lt;code&gt;*&lt;/code&gt; and exclusions can reduce omissions, while controllers excluded from &lt;code&gt;*&lt;/code&gt; still need explicit inclusion.&lt;/li&gt;
&lt;li&gt;Avoid overlapping controller assignments. Independent locks allow both groups' leaders to act at the same time.&lt;/li&gt;
&lt;li&gt;Provide replicas for each group if each needs high availability.&lt;/li&gt;
&lt;li&gt;Expect duplicate informers, caches, and List/Watch traffic across processes.&lt;/li&gt;
&lt;li&gt;Decide where &lt;code&gt;serviceaccount-token-controller&lt;/code&gt; runs. Startup gives this controller special treatment before the others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Group controllers with shared informer needs where practical. Deployment and ReplicaSet share ReplicaSet/Pod data; Job, CronJob, and finished-Job TTL share Job data; endpoint controllers share Service and endpoint-related data. Pods are used widely, so splitting still tends to duplicate Pod caches.&lt;/p&gt;

&lt;p&gt;Garbage collection, Namespace deletion, and quota accounting also use metadata-oriented resource discovery and informers. Their memory behavior is broader than one workload API. Measure total process RSS and API traffic across all groups, rather than comparing only the largest individual process.&lt;/p&gt;

&lt;p&gt;The shared factory and startup logic are in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kube-controller-manager/app/controllermanager.go" rel="noopener noreferrer"&gt;controllermanager.go&lt;/a&gt;. Controller Manager Leader Migration, described in &lt;a href="https://github.com/kubernetes/enhancements/blob/8c2bd2ef10809249b4cec72145db9bb78430fb35/keps/sig-api-machinery/2436-controller-manager-leader-migration/README.md" rel="noopener noreferrer"&gt;KEP-2436&lt;/a&gt;, addresses coordinated migration between controller managers; it is a different mechanism from manually dividing responsibilities into groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharding one controller requires more than a second lock
&lt;/h3&gt;

&lt;p&gt;Splitting workload and Node controllers does not increase the capacity of a single Job controller beyond that group's resources and workers. To scale one controller horizontally, instances need disjoint ownership of reconciliation targets.&lt;/p&gt;

&lt;p&gt;Kubernetes v1.37.1 has no general kube-controller-manager flag that shards built-in controllers such as Job across arbitrary object subsets. That requires implementation support. If every shard still watches all objects, caches and incoming Watch traffic also remain duplicated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes/enhancements/blob/8c2bd2ef10809249b4cec72145db9bb78430fb35/keps/sig-api-machinery/5866-server-side-sharded-list-and-watch/README.md" rel="noopener noreferrer"&gt;KEP-5866: Server-side Sharded List and Watch&lt;/a&gt; addresses source-side filtering for partitioned clients. Its &lt;code&gt;ShardedListAndWatch&lt;/code&gt; gate is alpha and disabled by default in this release. Enabling it does not automatically turn existing built-in controllers into sharded controllers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the inventory as a debugging map
&lt;/h2&gt;

&lt;p&gt;Find the API object whose lifecycle stopped progressing, identify the responsible controller, and confirm that it is selected, allowed by feature gates, and actually running. Then distinguish queue waiting, slow reconciliation, API throttling, and API server latency before tuning.&lt;/p&gt;

&lt;p&gt;If throughput remains insufficient, increase appropriate workers and client limits with API server headroom in view. Split controller groups when process isolation is useful; consider object sharding only when one controller itself needs implementation-level horizontal scaling.&lt;/p&gt;

&lt;p&gt;The 49 names tell you what kube-controller-manager can run. Runtime settings and metrics tell you what your cluster is running and where it is spending time.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>observability</category>
      <category>sre</category>
    </item>
    <item>
      <title>Native Histograms in Kubernetes 1.37: Migration Results and SLI Design</title>
      <dc:creator>yosshi_</dc:creator>
      <pubDate>Wed, 21 Oct 2026 00:39:46 +0000</pubDate>
      <link>https://dev.to/yosshi_/native-histograms-in-kubernetes-137-migration-results-and-sli-design-5bh7</link>
      <guid>https://dev.to/yosshi_/native-histograms-in-kubernetes-137-migration-results-and-sli-design-5bh7</guid>
      <description>&lt;p&gt;Native histograms can remove thousands of &lt;code&gt;_bucket&lt;/code&gt; series from a Kubernetes monitoring stack. The harder part is making sure the dashboards, alerts, and remote storage still see the same requests afterward.&lt;/p&gt;

&lt;p&gt;I compared four Prometheus configurations against the same single-node kind cluster. The native-preferred configuration reduced head series by &lt;strong&gt;76.4%&lt;/strong&gt; and RSS by &lt;strong&gt;34.7%&lt;/strong&gt;. I also found queries that returned only one component's data, aggregations that returned an empty result, and histogram samples that never reached remote storage.&lt;/p&gt;

&lt;p&gt;This article assumes familiarity with Prometheus scraping and basic PromQL. It covers Kubernetes &lt;strong&gt;v1.37.0&lt;/strong&gt; and Prometheus &lt;strong&gt;v3.13.2&lt;/strong&gt;, using the measurements recorded in September 2026. The remote storage results apply to the specific versions listed below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, and what the experiment measured
&lt;/h2&gt;

&lt;p&gt;Kubernetes 1.37 makes the &lt;code&gt;NativeHistograms&lt;/code&gt; feature gate beta and enabled by default. Supported histograms expose both classic and native representations; Prometheus decides which to ingest. The gate does &lt;strong&gt;not&lt;/strong&gt; make every Kubernetes histogram native. See the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.0/staging/src/k8s.io/component-base/metrics/features/kube_features.go#L25-L44" rel="noopener noreferrer"&gt;Kubernetes feature gate definition&lt;/a&gt; and &lt;a href="https://kep.k8s.io/5808" rel="noopener noreferrer"&gt;KEP-5808&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Native histograms became stable in Prometheus 3.8.0. In the version used here, collection is controlled through scrape configuration, without a native-histogram feature flag.&lt;/p&gt;

&lt;p&gt;All four Prometheus instances scraped the same kube-apiserver and kubelet jobs every 15 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P1: Classic only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Classic histograms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P2: Both&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Classic and native representations where available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P3: Native preferred&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native where available; classic otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P4: Convert remaining classic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native where available; remaining classic converted to native histograms with custom buckets (NHCB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After approximately one hour:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;P1&lt;/th&gt;
&lt;th&gt;P2&lt;/th&gt;
&lt;th&gt;P3&lt;/th&gt;
&lt;th&gt;P4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Head series&lt;/td&gt;
&lt;td&gt;30,010&lt;/td&gt;
&lt;td&gt;31,113&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7,076&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,432&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot block total&lt;/td&gt;
&lt;td&gt;4,052 KiB&lt;/td&gt;
&lt;td&gt;3,976 KiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,140 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;752 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block &lt;code&gt;index&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2,580 KiB&lt;/td&gt;
&lt;td&gt;2,672 KiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;620 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;408 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block &lt;code&gt;chunks&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1,460 KiB&lt;/td&gt;
&lt;td&gt;1,292 KiB&lt;/td&gt;
&lt;td&gt;508 KiB&lt;/td&gt;
&lt;td&gt;332 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAL&lt;/td&gt;
&lt;td&gt;4,000 KiB&lt;/td&gt;
&lt;td&gt;4,772 KiB&lt;/td&gt;
&lt;td&gt;1,580 KiB&lt;/td&gt;
&lt;td&gt;1,392 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS&lt;/td&gt;
&lt;td&gt;190 MiB&lt;/td&gt;
&lt;td&gt;189 MiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;124 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;111 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Histogram metric families still stored as classic&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The block measurements came from a snapshot with &lt;code&gt;skip_head=false&lt;/code&gt;, which writes the Head to a block. They were &lt;strong&gt;not&lt;/strong&gt; measured after normal two-hour Head compaction or subsequent block compaction. WAL is the usage at that instant, not a long-term storage estimate.&lt;/p&gt;

&lt;p&gt;These are single-point measurements from a small cluster. Scrape timing, changes in the source metrics, GC, and process state introduce variation. In particular, P2's slightly lower RSS and chunk usage do not establish that storing both formats saves resources.&lt;/p&gt;

&lt;p&gt;For P3, the reductions relative to P1 were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Head series: &lt;strong&gt;76.4%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Block index: &lt;strong&gt;76.0%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Block chunks: &lt;strong&gt;65.2%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;RSS: &lt;strong&gt;34.7%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Series reduction is useful, but it is not a memory or storage sizing formula.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the series count falls
&lt;/h2&gt;

&lt;p&gt;A classic histogram stores one float series per bucket boundary, plus &lt;code&gt;_sum&lt;/code&gt; and &lt;code&gt;_count&lt;/code&gt;. For a histogram with 20 bucket boundaries and 100,000 label combinations, that is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;label_combinations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;
&lt;span class="n"&gt;bucket_boundaries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="n"&gt;classic_series&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_combinations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_boundaries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 2,200,000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A native histogram stores the distribution as one histogram sample per label combination:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;native_series&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_combinations&lt;/span&gt;  &lt;span class="c1"&gt;# 100,000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The bucket counts move inside the sample. Increasing resolution therefore increases &lt;strong&gt;sample size&lt;/strong&gt;, rather than creating more time series.&lt;/p&gt;

&lt;p&gt;This explains the difference between index and chunk savings. The index contains series labels and postings; fewer series remove much of that overhead. Chunks store the samples themselves, and a histogram sample is larger than a float. WAL, network traffic, and in-memory chunks also depend on sample size.&lt;/p&gt;

&lt;p&gt;Ordinary labels still matter. A &lt;code&gt;route&lt;/code&gt; label with 5,000 distinct values continues to create 5,000 label combinations. Native histograms do not fix unbounded URLs, user IDs, or request IDs in labels.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the distributions look like
&lt;/h3&gt;

&lt;p&gt;Grafana Labs illustrates the difference using response-time heatmaps and histograms from the same application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foi739pluit7xfa6h3wzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foi739pluit7xfa6h3wzk.png" alt="Native response-time distribution with fine buckets" width="798" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2w9hlzaan6361ltlq0mq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2w9hlzaan6361ltlq0mq.png" alt="Classic response-time distribution with preset bucket boundaries" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://grafana.com/blog/prometheus-native-histograms-in-grafana-cloud-more-precise-easier-to-use-and-better-compatibility/" rel="noopener noreferrer"&gt;Grafana Labs — Prometheus native histograms in Grafana Cloud&lt;/a&gt;. These images come from Grafana's example, not the kind experiment. Axis ranges, scales, and displayed time windows differ, so compare the visible structure rather than bar heights or colors numerically. Classic histograms can also use finer boundaries, at the cost of more series.&lt;/p&gt;
&lt;h3&gt;
  
  
  Exponential buckets provide consistent relative resolution
&lt;/h3&gt;

&lt;p&gt;For standard exponential native histograms, adjacent boundaries have the ratio:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Kubernetes configures a bucket factor of &lt;code&gt;1.1&lt;/code&gt;, which client_golang maps to &lt;strong&gt;schema 3&lt;/strong&gt;. Its actual growth factor is approximately &lt;code&gt;1.0905&lt;/code&gt;: eight buckets per doubling, with a relative width of about 9.05% measured from the lower boundary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Buckets per doubling&lt;/th&gt;
&lt;th&gt;Relative bucket width&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;18.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9.05%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;4.43%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;0.543%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The boundaries nest. Combining adjacent pairs of schema 3 buckets produces schema 2 buckets by adding their counts, without interpolation. You can reduce resolution exactly at the bucket-count level, but you cannot recover the finer distribution afterward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwdb3oufgozk6ml27vkx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwdb3oufgozk6ml27vkx.png" alt="Four schema 3 buckets merge in pairs into two schema 2 buckets" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Illustrative counts, with rounded boundaries. The diagram follows the &lt;a href="https://prometheus.io/docs/specs/native_histograms/#schema" rel="noopener noreferrer"&gt;Prometheus schema specification&lt;/a&gt; and &lt;a href="https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponential-scale" rel="noopener noreferrer"&gt;OpenTelemetry exponential scale definition&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sparse encoding avoids materializing the entire theoretical range. In the Mimir measurements, 1,259 active native histogram series contained 17,064 active buckets: an average of &lt;strong&gt;13.6 buckets per series&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://prometheus.io/docs/specs/native_histograms/" rel="noopener noreferrer"&gt;Prometheus native histogram specification&lt;/a&gt; explains the data model, schemas, and encoding. NHCB uses a different schema: it retains explicitly supplied bucket boundaries rather than exponential ones.&lt;/p&gt;
&lt;h2&gt;
  
  
  Enable collection, then rewrite the queries
&lt;/h2&gt;

&lt;p&gt;For an existing job, P3 requires this change:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes-apiservers&lt;/span&gt;
    &lt;span class="na"&gt;scrape_native_histograms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;always_scrape_classic_histograms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="c1"&gt;# Keep the existing discovery, authentication, and relabeling settings.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To keep classic series during migration, set &lt;code&gt;always_scrape_classic_histograms: true&lt;/code&gt; for P2. To convert classic-only histograms to NHCB, also set &lt;code&gt;convert_classic_histograms_to_nhcb: true&lt;/code&gt; for P4.&lt;/p&gt;

&lt;p&gt;The first three switches can be set globally or per job. Starting with a job limits the scope of the change. Prometheus also supports target-level overrides through relabeling. The options are documented in the &lt;a href="https://github.com/prometheus/prometheus/blob/v3.13.2/docs/configuration/configuration.md" rel="noopener noreferrer"&gt;v3.13.2 configuration reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the exporter libraries and exposition formats used in this experiment, native histogram collection requires protobuf negotiation. Enabling &lt;code&gt;scrape_native_histograms&lt;/code&gt; makes protobuf preferred by default. If you explicitly configure &lt;code&gt;scrape_protocols&lt;/code&gt;, check that &lt;code&gt;PrometheusProto&lt;/code&gt; is preferred. A text response can contain only the classic representation even when the exporter supports native histograms.&lt;/p&gt;
&lt;h3&gt;
  
  
  The base name replaces the suffix series
&lt;/h3&gt;

&lt;p&gt;When classic storage is disabled for a native-capable metric, the queryable series change:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Classic&lt;/th&gt;
&lt;th&gt;Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X_bucket{le="..."}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X&lt;/code&gt;, containing a histogram sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X_sum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extract with &lt;code&gt;histogram_sum()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extract with &lt;code&gt;histogram_count()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, preserve the &lt;code&gt;job&lt;/code&gt; grouping while removing &lt;code&gt;le&lt;/code&gt;:&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Classic p99 per job
&lt;/span&gt;&lt;span class="nf"&gt;histogram_quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;sum&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiserver_request_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Native p99 per job
&lt;/span&gt;&lt;span class="nf"&gt;histogram_quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;sum&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiserver_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Other common replacements are:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Request rate
&lt;/span&gt;&lt;span class="nf"&gt;histogram_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="c1"&gt;# Mean duration per input series
&lt;/span&gt;&lt;span class="nf"&gt;histogram_avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="c1"&gt;# Fraction at or below 500 ms, aggregated across the selected series
&lt;/span&gt;&lt;span class="nf"&gt;histogram_fraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Inf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Apply &lt;code&gt;rate()&lt;/code&gt; before aggregation so resets are handled per series. Preserve the labels that define the service or operation you intend to measure.&lt;/p&gt;

&lt;p&gt;A histogram is not a float. This comparison produces no usable alert result:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Extract a float first:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;histogram_quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Audit dashboards, recording rules, alert rules, and queries against remote storage. A syntactically valid query can still return nothing after its &lt;code&gt;_bucket&lt;/code&gt;, &lt;code&gt;_sum&lt;/code&gt;, or &lt;code&gt;_count&lt;/code&gt; series disappear.&lt;/p&gt;
&lt;h2&gt;
  
  
  The subtle failure: a query returns only one component
&lt;/h2&gt;

&lt;p&gt;Of the 74 classic histogram metric families observed in the two jobs, &lt;strong&gt;47 became native and 27 remained classic&lt;/strong&gt; in P3.&lt;/p&gt;

&lt;p&gt;Some remaining histograms use a different implementation, such as &lt;code&gt;TimingHistogram&lt;/code&gt;; others come from the Go collector rather than Kubernetes component-base. A third group is affected by initialization order.&lt;/p&gt;

&lt;p&gt;The same metric name can therefore have different representations in different components:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;kubelet&lt;/th&gt;
&lt;th&gt;kube-apiserver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_work_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Classic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workqueue_queue_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Classic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rest_client_request_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Classic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This query returns kubelet data only:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workqueue_work_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The kube-apiserver observations live in &lt;code&gt;workqueue_work_duration_seconds_bucket&lt;/code&gt; and its companion series. Prometheus has no reason to warn that the native query omitted a component you expected to include.&lt;/p&gt;

&lt;p&gt;Check coverage before replacing a query:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Jobs exposing the base-name series
&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workqueue_work_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Jobs exposing classic buckets
&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workqueue_work_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In kube-apiserver v1.37.0, informers and REST clients can be constructed before &lt;code&gt;ApplyFeatureGates()&lt;/code&gt; propagates the gate state to the metrics subsystem. Histograms initialized at that point remain classic, while later ones become native. I reported this in &lt;a href="https://github.com/kubernetes/kubernetes/issues/142379" rel="noopener noreferrer"&gt;kubernetes/kubernetes#142379&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  NHCB changes the failure mode
&lt;/h3&gt;

&lt;p&gt;NHCB packages classic bucket counts into native histogram samples while retaining their original boundaries. It reduces series count, but &lt;strong&gt;does not improve resolution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;P4 converts the remaining kube-apiserver workqueue histogram to custom buckets, while kubelet still exposes exponential buckets. Aggregating the two produces a warning and an empty result:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workqueue_work_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"PromQL warning: vector contains a mix of histograms with exponential and custom buckets schemas for metric name &lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt; (1:5)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The original kind experiment found eight metric names with this problem. All eight returned an empty result when aggregated across the incompatible schemas.&lt;/p&gt;

&lt;p&gt;Keep the aggregation groups separate, for example by &lt;code&gt;job&lt;/code&gt;, and inspect API warnings. NHCB is useful for classic-only exporters you cannot change, provided the aggregation and storage paths support it. It is not an automatic final step for every migration.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remote storage can undo the migration
&lt;/h2&gt;

&lt;p&gt;There are two separate questions: did Prometheus send the samples, and how did the receiver store them?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5l5gzc8b3umf1yhyugnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5l5gzc8b3umf1yhyugnq.png" alt="Prometheus stores locally and sends to remote storage, where ingestion and queries must also be checked" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Local collection and long-term availability require separate checks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Remote write v1 does not send native histograms by default
&lt;/h3&gt;

&lt;p&gt;For the default v1 protocol, explicitly enable sending:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;remote_write&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://thanos-receive:19291/api/v1/receive&lt;/span&gt;
    &lt;span class="na"&gt;send_native_histograms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Without this option, Prometheus can store histograms locally while sending none of them to remote storage. The v3.13.2 &lt;a href="https://github.com/prometheus/prometheus/blob/v3.13.2/storage/remote/queue_manager.go#L844-L861" rel="noopener noreferrer"&gt;queue implementation&lt;/a&gt; returns early without a log or a dropped-sample increment for this disabled path.&lt;/p&gt;

&lt;p&gt;If classic storage has also been disabled, the long-term store loses that histogram entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NHCB requires remote write v2.&lt;/strong&gt; For a receiver that supports it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;remote_write&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://mimir:8080/api/v1/push&lt;/span&gt;
    &lt;span class="na"&gt;protobuf_message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;io.prometheus.write.v2.Request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With v2, native histogram sending is always enabled; &lt;code&gt;send_native_histograms&lt;/code&gt; is a no-op. With v1 and native sending enabled, NHCB samples are dropped with a warning and the reason &lt;code&gt;nhcb_in_rw1_not_supported&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Receiver behavior differs
&lt;/h3&gt;

&lt;p&gt;These results are scoped to the tested versions, rather than a claim about every release:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Receiver&lt;/th&gt;
&lt;th&gt;Exponential native histograms&lt;/th&gt;
&lt;th&gt;NHCB over remote write&lt;/th&gt;
&lt;th&gt;Remote write v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Thanos Receive v0.41.0&lt;/td&gt;
&lt;td&gt;Stored as histogram samples&lt;/td&gt;
&lt;td&gt;Unavailable through this v1-only Receive path&lt;/td&gt;
&lt;td&gt;Unsupported in this version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana Mimir 3.2.1&lt;/td&gt;
&lt;td&gt;Stored as histogram samples&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported, experimental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VictoriaMetrics v1.152.0&lt;/td&gt;
&lt;td&gt;Converted into &lt;code&gt;_count&lt;/code&gt;, &lt;code&gt;_sum&lt;/code&gt;, and &lt;code&gt;_bucket&lt;/code&gt; series with &lt;code&gt;vmrange&lt;/code&gt; labels&lt;/td&gt;
&lt;td&gt;Unsupported&lt;/td&gt;
&lt;td&gt;Unsupported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjrz4gbprv6ei28vrdb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjrz4gbprv6ei28vrdb4.png" alt="P3 sends exponential native histograms to three receivers; P4 drops NHCB on the v1 sender path" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The P4-to-Thanos path shows the additional v1 experiment: exponential samples were sent, while NHCB samples were dropped by Prometheus.&lt;/p&gt;

&lt;p&gt;Thanos Receive ingested &lt;strong&gt;12,580 histogram samples&lt;/strong&gt; without an additional native histogram flag. Its &lt;a href="https://github.com/thanos-io/thanos/blob/v0.41.0/pkg/receive/handler.go" rel="noopener noreferrer"&gt;v0.41.0 Receive implementation&lt;/a&gt; uses the v1 request type.&lt;/p&gt;

&lt;p&gt;Mimir reported &lt;strong&gt;1,259 native histogram series&lt;/strong&gt; among 7,435 active series. When using query sharding in this version, its help text requires &lt;code&gt;-query-frontend.query-result-response-format=protobuf&lt;/code&gt; for native histogram queries. This is an internal response-format requirement, separate from the public JSON query API.&lt;/p&gt;

&lt;p&gt;VictoriaMetrics expanded the distributions back into bucket series. Comparing separate receivers fed classic and native data gave:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VictoriaMetrics measurement&lt;/th&gt;
&lt;th&gt;Classic input&lt;/th&gt;
&lt;th&gt;Native input&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;totalSeries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;31,812&lt;/td&gt;
&lt;td&gt;25,283&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;totalLabelValuePairs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;255,703&lt;/td&gt;
&lt;td&gt;191,272&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9scszcybcr1dk70j8coc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9scszcybcr1dk70j8coc.png" alt="VictoriaMetrics converts one native histogram series into count, sum, and multiple vmrange bucket series" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conversion during ingestion reintroduces multiple float series per distribution.&lt;/p&gt;

&lt;p&gt;The receiver's series reduction was &lt;strong&gt;20.5%&lt;/strong&gt;, compared with 76.4% in the Prometheus Head. Some metrics gained bucket series because more converted buckets were populated than the original classic definition contained. The conversion is visible in the &lt;a href="https://github.com/VictoriaMetrics/VictoriaMetrics/blob/v1.152.0/lib/prompb/write_request_unmarshaler.go#L377-L380" rel="noopener noreferrer"&gt;v1.152.0 ingestion implementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Head series and &lt;code&gt;totalSeries&lt;/code&gt; cover different populations and time spans. These counts also do not measure stored bytes. Measure the receiving system's storage and query behavior before projecting long-term savings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F456eoe6vruclimvwri2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F456eoe6vruclimvwri2e.png" alt="Storage mode choices for Mimir, Thanos, VictoriaMetrics, and local-only storage" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These choices apply to the tested versions. For P4, also verify aggregation compatibility between exponential and custom bucket schemas.&lt;/p&gt;
&lt;h2&gt;
  
  
  Better resolution does not make p99.9 stable
&lt;/h2&gt;

&lt;p&gt;The migration changes what distributions you can retain economically. It does not create more requests.&lt;/p&gt;

&lt;p&gt;There are two different sources of uncertainty:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What affects it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inferring typical or future behavior from a finite observation window&lt;/td&gt;
&lt;td&gt;Request count, window length, distribution changes, and dependence between requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approximating recorded values with buckets&lt;/td&gt;
&lt;td&gt;Bucket boundaries, resolution, and interpolation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Increasing &lt;code&gt;schema&lt;/code&gt; addresses the second. It cannot resolve the first.&lt;/p&gt;
&lt;h3&gt;
  
  
  Count the observations in the tail
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;N&lt;/code&gt; requests and quantile &lt;code&gt;p&lt;/code&gt;, the expected number above a fixed population quantile is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantile&lt;/th&gt;
&lt;th&gt;Tail fraction&lt;/th&gt;
&lt;th&gt;Tail count with 1,000 requests&lt;/th&gt;
&lt;th&gt;With 100,000 requests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99.9&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With 1,000 requests, p99.9 is effectively tracking the very slowest observations. A finer bucket layout cannot make those observations repeat consistently.&lt;/p&gt;

&lt;p&gt;Under independent observations from a fixed distribution, the count above a fixed population quantile is binomial. Its relative standard deviation is &lt;code&gt;sqrt(p/k)&lt;/code&gt;, approximately &lt;code&gt;1/sqrt(k)&lt;/code&gt; for high quantiles. At &lt;code&gt;k = 1&lt;/code&gt;, that is about 100%; at &lt;code&gt;k = 100&lt;/code&gt;, about 10%. See the &lt;a href="https://www.itl.nist.gov/div898/handbook/eda/section3/eda366i.htm" rel="noopener noreferrer"&gt;NIST description of the binomial distribution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Those percentages describe &lt;strong&gt;count variation&lt;/strong&gt;, not percentage error in the latency value. They cannot be directly compared with a 9.05% bucket width. The value uncertainty also depends on the density of observations near the quantile.&lt;/p&gt;

&lt;p&gt;Using 100 expected tail observations as a planning heuristic gives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantile&lt;/th&gt;
&lt;th&gt;Required requests&lt;/th&gt;
&lt;th&gt;Average rate over one hour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p95&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;0.56 requests/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;2.8 requests/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99.9&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;27.8 requests/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a way to reason about window length, not an accuracy guarantee. Correlated traffic or a changing workload can still make the result unstable.&lt;/p&gt;

&lt;p&gt;Scraping is not random request sampling: counters and histograms normally accumulate events between scrapes. Keep finite-window uncertainty separate from bucket approximation, missed scrapes, resets, and &lt;code&gt;rate()&lt;/code&gt; extrapolation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Threshold SLIs still need an error model
&lt;/h3&gt;

&lt;p&gt;A threshold SLI expresses the fraction of requests that meet a latency target:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;histogram_fraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Inf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_request_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This fits the &lt;code&gt;good events / total events&lt;/code&gt; model used for error budgets. It does not eliminate low-traffic variation.&lt;/p&gt;

&lt;p&gt;It also introduces interpolation when the threshold is inside a bucket. A factor of &lt;code&gt;1.1&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; guarantee that the estimated good-event fraction is within 10% of the true fraction. If observations cluster near the threshold, even a narrow bucket can contain enough requests to materially change the result.&lt;/p&gt;

&lt;p&gt;For exact threshold counts, retain a classic boundary at the SLO threshold or instrument dedicated good/total counters. Prometheus v3.13.2 also supports &lt;code&gt;histogram_fraction()&lt;/code&gt; for classic histograms, so estimating arbitrary thresholds is not exclusive to native histograms. Native exponential buckets make relatively fine boundaries available across a broad range.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://prometheus.io/docs/practices/histograms/#errors-of-quantile-estimation" rel="noopener noreferrer"&gt;Prometheus histogram guidance&lt;/a&gt; explains value-domain and quantile-domain errors. The &lt;a href="https://sre.google/workbook/implementing-slos/" rel="noopener noreferrer"&gt;Google SRE Workbook&lt;/a&gt; discusses multiple latency thresholds to capture both typical experience and the long tail.&lt;/p&gt;
&lt;h3&gt;
  
  
  A larger p99 does not automatically mean worse performance
&lt;/h3&gt;

&lt;p&gt;In the P2 experiment, &lt;code&gt;apiserver_request_duration_seconds&lt;/code&gt; produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query result&lt;/th&gt;
&lt;th&gt;Classic&lt;/th&gt;
&lt;th&gt;Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean&lt;/td&gt;
&lt;td&gt;4.13 s&lt;/td&gt;
&lt;td&gt;4.17 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;318.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The classic definition's largest finite boundary was 60 seconds. When the requested quantile falls in &lt;code&gt;+Inf&lt;/code&gt;, &lt;code&gt;histogram_quantile()&lt;/code&gt; returns the previous bucket's upper bound. Native buckets could represent observations beyond that boundary.&lt;/p&gt;

&lt;p&gt;This is evidence of a representation limit, not a comparison against raw-event ground truth. The aggregate also included long-running requests such as watches. It is not an example of an appropriate SLI for ordinary API calls. Define the operations and labels first, then compare the same scope and observation window.&lt;/p&gt;
&lt;h2&gt;
  
  
  Instrumenting a Go exporter
&lt;/h2&gt;

&lt;p&gt;For client_golang, native support starts in v1.14.0. The example below uses the options in &lt;strong&gt;v1.24.0&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;requestDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHistogramVec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HistogramOpts&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"myapp_request_duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"Request duration in seconds."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Buckets&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;existingBuckets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;NativeHistogramBucketFactor&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="m"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;NativeHistogramMaxBucketNumber&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;NativeHistogramMinResetDuration&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Keep the existing classic buckets during migration. Observe each event once; the histogram maintains both representations. &lt;code&gt;promhttp.Handler()&lt;/code&gt; can negotiate the protobuf response.&lt;/p&gt;

&lt;p&gt;If native histograms are enabled and &lt;code&gt;Buckets&lt;/code&gt; is omitted, client_golang does not fill in its default finite classic buckets. Text exposition may still expose &lt;code&gt;_sum&lt;/code&gt;, &lt;code&gt;_count&lt;/code&gt;, and &lt;code&gt;+Inf&lt;/code&gt;; do not assume every classic-related series disappears from the endpoint.&lt;/p&gt;
&lt;h3&gt;
  
  
  Choose the bucket limit from the observed range
&lt;/h3&gt;

&lt;p&gt;For positive values, a rough estimate of the buckets needed to densely cover a range is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log2&lt;/span&gt;

&lt;span class="c1"&gt;# Approximate bucket count; both expressions are equivalent.
&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vmax&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;Vmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vmax&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;Vmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At schema 3:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Approximate bucket count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 ms to 30 s&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1 ms to 60 s&lt;/td&gt;
&lt;td&gt;154&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 byte to 100 MB&lt;/td&gt;
&lt;td&gt;213&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 byte to 1 GB&lt;/td&gt;
&lt;td&gt;239&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sparse observations may occupy far fewer buckets. The estimate helps identify distributions where a limit of 160 could force coarser resolution, especially size metrics spanning many orders of magnitude.&lt;/p&gt;
&lt;h3&gt;
  
  
  Recovery from reduced resolution has a cost
&lt;/h3&gt;

&lt;p&gt;In client_golang v1.24.0, exceeding &lt;code&gt;NativeHistogramMaxBucketNumber&lt;/code&gt; can trigger a reset, widening of the zero bucket, or reduction of the schema. With &lt;code&gt;NativeHistogramMinResetDuration&lt;/code&gt; left at zero, the reset-based recovery path is disabled. A reduced schema does not recover through that mechanism while the same histogram instance remains alive.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;time.Hour&lt;/code&gt; allows a reset after the minimum interval when needed; it is not an unconditional hourly reset. The reset affects the whole histogram, including classic cumulative values. &lt;code&gt;rate()&lt;/code&gt; can correct an observed reset, but cannot recover observations accumulated after the preceding scrape and lost before the reset was scraped.&lt;/p&gt;

&lt;p&gt;Kubernetes v1.37.0 component-base configures factor &lt;code&gt;1.1&lt;/code&gt; and limit &lt;code&gt;160&lt;/code&gt;, but leaves the minimum reset duration unset. I raised the tradeoff in &lt;a href="https://github.com/kubernetes/kubernetes/issues/142377" rel="noopener noreferrer"&gt;kubernetes/kubernetes#142377&lt;/a&gt;. For your own exporter, choose the limit and reset policy together, using the &lt;a href="https://github.com/prometheus/client_golang/blob/v1.24.0/prometheus/histogram.go#L429-L482" rel="noopener noreferrer"&gt;client_golang v1.24.0 option documentation and implementation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  A rollout that checks data coverage
&lt;/h2&gt;

&lt;p&gt;The first migration task is an inventory of metric names and jobs, followed by queries and remote storage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find classic families with &lt;code&gt;count by (__name__) ({__name__=~".+_bucket"})&lt;/code&gt; and locate their consumers in dashboards, recording rules, and alerts.&lt;/li&gt;
&lt;li&gt;Collect both formats for a bounded comparison period if the impact is broad. Compare counts, sums, and means over matching labels and windows. Expect quantiles to differ when boundaries or interpolation differ.&lt;/li&gt;
&lt;li&gt;Check which jobs expose each representation. Verify that rewritten aggregations still include every intended component.&lt;/li&gt;
&lt;li&gt;Check native sending and query the receiver directly. For NHCB, verify v2 support and keep incompatible schemas out of the same aggregation group.&lt;/li&gt;
&lt;li&gt;Disable classic storage for native-capable metrics once those checks pass. Keep access to the classic queries for historical data until it ages out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A direct P1-to-P3 switch can also work when the affected queries and storage paths are already known and checked. Configuration reloads change future collection; they do not convert existing samples.&lt;/p&gt;

&lt;p&gt;For SLI design, decide the user-facing latency thresholds and required success fractions first. Then choose a window with enough observations and a bucket layout that meets the required approximation accuracy. Native histograms make broad distributions cheaper to retain, but neither tail traffic nor threshold accuracy comes for free.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Experiment versions and measurement commands
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes / kind node image&lt;/td&gt;
&lt;td&gt;v1.37.0 / &lt;code&gt;kindest/node:v1.37.0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kind&lt;/td&gt;
&lt;td&gt;v0.33.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus&lt;/td&gt;
&lt;td&gt;v3.13.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;client_golang implementation reference&lt;/td&gt;
&lt;td&gt;v1.24.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thanos Receive&lt;/td&gt;
&lt;td&gt;v0.41.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana Mimir, single binary&lt;/td&gt;
&lt;td&gt;3.2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VictoriaMetrics&lt;/td&gt;
&lt;td&gt;v1.152.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The kind cluster used one control-plane node. The local container environment was allocated eight CPUs and 16 GiB of memory for the four Prometheus instances and supporting services.&lt;/p&gt;

&lt;p&gt;Head series were read from the TSDB status API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; nh-verify &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/prom-native &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:9090/api/v1/status/tsdb'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The admin API snapshot included Head data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; nh-verify &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/prom-native &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; &lt;span class="nt"&gt;--post-data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'http://localhost:9090/api/v1/admin/tsdb/snapshot?skip_head=false'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Block, index, chunk, and WAL sizes were measured with &lt;code&gt;du -sk&lt;/code&gt; and reported in KiB. The other three instances used the same jobs and interval with the histogram switches changed according to P1–P4.&lt;/p&gt;

&lt;p&gt;The experiment did not measure transfer bytes, long-term compacted storage, or quantile accuracy against raw request observations. Its results establish a comparison for this workload, rather than a production sizing ratio.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>prometheus</category>
      <category>observability</category>
      <category>sre</category>
    </item>
    <item>
      <title>An Overview of Agent Substrate</title>
      <dc:creator>yosshi_</dc:creator>
      <pubDate>Tue, 20 Oct 2026 10:44:28 +0000</pubDate>
      <link>https://dev.to/yosshi_/an-overview-of-agent-substrate-501d</link>
      <guid>https://dev.to/yosshi_/an-overview-of-agent-substrate-501d</guid>
      <description>&lt;p&gt;A Kubernetes Pod is a useful unit of deployment. An agent session has a different lifetime: it may run briefly, wait for minutes, then need its process and filesystem state back for the next turn. Keeping a dedicated environment resident through every wait can consume capacity that other sessions could use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/agent-substrate/substrate" rel="noopener noreferrer"&gt;Agent Substrate&lt;/a&gt; separates those lifetimes. Kubernetes runs a pool of worker Pods; Substrate places sandboxed agent environments onto them, persists their state, and restores them when work arrives. This article follows one request through that system, then examines the components and a Counter demo you can try locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes manages Workers; Substrate manages Actors.&lt;/strong&gt; Actor state and assignments live behind a separate API backed by PostgreSQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers can serve many Actors over time.&lt;/strong&gt; Capacity-based concurrency is combined with suspend/resume to reuse compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;atelet&lt;/code&gt; and &lt;code&gt;ateom&lt;/code&gt; save and restore sandbox state.&lt;/strong&gt; They use gVisor or microVMs; the snapshot fidelity determines what is preserved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials can stay outside the Actor.&lt;/strong&gt; An egress gateway and credential provider can inject API credentials into outgoing requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the implemented behavior, not just the design.&lt;/strong&gt; Authorization enforcement is experimental and disabled by default for ordinary Control RPCs. Idle detection and performance claims also require context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementation details target &lt;strong&gt;Agent Substrate v0.5.0&lt;/strong&gt;. See Components and versions for the full scope.&lt;/p&gt;

&lt;p&gt;This walkthrough assumes familiarity with Pods, Deployments and basic Kubernetes networking. Start with the request lifecycle, jump to the kind demo, or use the evaluation questions to assess the fit for your platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision: keep the workers, move the sessions
&lt;/h2&gt;

&lt;p&gt;An agent may execute code briefly, wait for inference or an external tool, then wait indefinitely for another user turn. Running untrusted code also creates a need for isolated environments, often with one tenant per sandbox.&lt;/p&gt;

&lt;p&gt;The combination matters: a large fleet can contain many more waiting environments than actively executing ones. Reserving CPU and memory for every waiting agent wastes capacity. Snapshot-based multiplexing is intended to let those environments retain state without remaining assigned to running compute.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L17-L23" rel="noopener noreferrer"&gt;docs/architecture.md#L17-L23&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Kubernetes still does—and what Substrate moves elsewhere
&lt;/h3&gt;

&lt;p&gt;The architecture document identifies three pressures behind its design:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pressure&lt;/th&gt;
&lt;th&gt;Why it matters for agents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API resource count and write traffic&lt;/td&gt;
&lt;td&gt;Millions of frequently changing agent records are a different workload from ordinary Kubernetes reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activation latency&lt;/td&gt;
&lt;td&gt;Pod scheduling, reconciliation, network hops and image preparation add overhead to very short executions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State movement&lt;/td&gt;
&lt;td&gt;Rapidly saving and restoring many environments is a different problem from attaching conventional persistent volumes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the project's design judgments for its intended scale, rather than universal Kubernetes limits or a claim that every Pod takes seconds to start.&lt;/p&gt;

&lt;p&gt;Kubernetes still handles infrastructure provisioning and worker Pod lifecycles. Substrate introduces a separate control plane for Actor state, assignment and resumption, so an activation does not need to wait for a fresh Pod lifecycle.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L153" rel="noopener noreferrer"&gt;docs/architecture.md#L153&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L160-L172" rel="noopener noreferrer"&gt;docs/architecture.md#L160-L172&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The important boundary is the activation path: a new turn can restore an Actor onto an existing Worker without provisioning another Pod. Database write throughput, reconciliation and environment preparation all contribute to that choice.&lt;/p&gt;
&lt;h2&gt;
  
  
  Follow a request into a suspended Actor
&lt;/h2&gt;

&lt;p&gt;The execution layer combines routing, state persistence and sandbox isolation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8pz60ez4cdbppufxgic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8pz60ez4cdbppufxgic.png" alt="Agent Substrate adds a gateway, state persistence and kernel isolation above Kubernetes, compute and storage." width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/agent-substrate-available-on-gke" rel="noopener noreferrer"&gt;Agent Substrate on GKE, Google Cloud Blog (September 16, 2026)&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Two lifecycles, two APIs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Managed through&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ActorTemplate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Immutable workload definition, including images and snapshot settings&lt;/td&gt;
&lt;td&gt;Substrate API and database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WorkerPool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines a pool that &lt;code&gt;atecontroller&lt;/code&gt; reconciles into a Deployment&lt;/td&gt;
&lt;td&gt;Kubernetes CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Atespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logical grouping and identity scope for Actors; not a Kubernetes namespace&lt;/td&gt;
&lt;td&gt;Substrate API and database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Actor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An instance of an ActorTemplate and the unit of suspend/resume&lt;/td&gt;
&lt;td&gt;Substrate API and database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Worker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A record corresponding to a worker Pod; can host multiple Actors concurrently&lt;/td&gt;
&lt;td&gt;Substrate API and database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/glossary.md#L10-L23" rel="noopener noreferrer"&gt;docs/glossary.md#L10-L23&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/glossary.md#L38-L52" rel="noopener noreferrer"&gt;docs/glossary.md#L38-L52&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;Actor&lt;/strong&gt; is the stateful environment that belongs to a session. A &lt;strong&gt;Worker&lt;/strong&gt; is the reusable execution capacity. Suspending an Actor releases its assignment; it does not require destroying and recreating the worker Pod. Worker capacity can also permit several Actors to run concurrently.&lt;/p&gt;
&lt;h3&gt;
  
  
  Restore first, forward second
&lt;/h3&gt;

&lt;p&gt;For an HTTP request targeting a suspended Actor, the activation path has four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The router identifies the Actor from &lt;code&gt;ate-target-actor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The control plane assigns a Worker and coordinates restoration of the Actor's snapshot.&lt;/li&gt;
&lt;li&gt;Once the assignment is ready, the router forwards the request through the Worker's tunnel to the Actor.&lt;/li&gt;
&lt;li&gt;The Actor handles the request and returns its response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For an Actor that is already running, &lt;code&gt;ResumeActor&lt;/code&gt; returns the existing assignment. The call is idempotent.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/internal/controlapi/workflow_resume.go#L67-L113" rel="noopener noreferrer"&gt;workflow_resume.go&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Returning a response does not automatically suspend the Actor. The application or higher-level system must choose a safe suspension boundary, taking account of outstanding work and network calls.&lt;/p&gt;
&lt;h3&gt;
  
  
  Suspend and resume
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1i9gqsf94gn8oa44djh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1i9gqsf94gn8oa44djh.png" alt="Active agent environments on Workers exchange state with idle environments persisted in storage through suspend/resume." width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/agent-substrate-available-on-gke" rel="noopener noreferrer"&gt;Agent Substrate on GKE, Google Cloud Blog (September 16, 2026)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here, “idle” means state has been persisted and the environment removed from running compute, rather than a process simply sleeping. The illustration is conceptual: it does not imply a fixed one-to-one relationship between Workers and Actors.&lt;/p&gt;

&lt;p&gt;When a request arrives, &lt;code&gt;atenet-router&lt;/code&gt; asks the control plane to resume the target Actor and resolve its Worker assignment. A suspended Actor is restored from its latest snapshot before traffic is forwarded. It may resume on a different Worker.&lt;/p&gt;

&lt;p&gt;The router itself does not decide whether the Actor is running; that decision belongs to the control plane. Suspending the Actor saves state and releases its assignment for reuse.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L193-L204" rel="noopener noreferrer"&gt;docs/architecture.md#L193-L204&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What runs where
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Relationship to Kubernetes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ate-api-server&lt;/code&gt; (&lt;code&gt;ateapi&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Actor lifecycle, scheduling and snapshot coordination&lt;/td&gt;
&lt;td&gt;Separate API, scheduler and workflow layer backed by PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;atecontroller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reconciles WorkerPool and related CRDs&lt;/td&gt;
&lt;td&gt;Standard controller-runtime controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;atelet&lt;/code&gt; / &lt;code&gt;ateom&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Coordinates and executes sandbox checkpoint/restore&lt;/td&gt;
&lt;td&gt;Additional execution layer inside worker Pod boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;atenet&lt;/code&gt; router and egress&lt;/td&gt;
&lt;td&gt;Resumption-aware ingress and controlled outbound traffic&lt;/td&gt;
&lt;td&gt;Routing plus Actor lifecycle coordination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podcertcontroller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issues short-lived certificates for component mTLS&lt;/td&gt;
&lt;td&gt;Signer controller for &lt;code&gt;PodCertificateRequest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The threat-model diagram shows how these components connect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5i5w2qe9o925i945nu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5i5w2qe9o925i945nu3.png" alt="Substrate architecture showing ate-api-server, atelet, ateom, atenet-router, object storage and PostgreSQL." width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/threat-model.md#L30-L40" rel="noopener noreferrer"&gt;docs/threat-model.md#L30-L40&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Repository map: controllers, sidecars and tools
  &lt;p&gt;The repository also includes supporting programs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/ateapi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gRPC control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/atecontroller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes controllers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/atelet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Node supervisor deployed as a DaemonSet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/ateom-gvisor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Drives gVisor inside worker Pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/ateom-microvm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs Actors with Cloud Hypervisor microVMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/objectstore-plugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Snapshot transfer sidecar for node and control-plane operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/atenet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Networking daemon and proxy integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/dataplane/envoy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Envoy image with Rust Dynamic Modules for egress enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/credential-provider/kubernetes-secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reference credential provider backed by Kubernetes Secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/podcertcontroller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pod certificate signers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/kubectl-ate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Actor and other Substrate resource operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/ate-setup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installation and removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd/benchmarking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthetic resource-consuming test workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/setup-gcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GKE, storage and IAM provisioning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The README's tour is not an exhaustive inventory; the sidecar, provider, dataplane and setup entries are supplemented from their own source and documentation.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/README.md#L260-L272" rel="noopener noreferrer"&gt;README.md#L260-L272&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/objectstore-plugin/main.go#L15-L19" rel="noopener noreferrer"&gt;cmd/objectstore-plugin/main.go#L15-L19&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ate-setup/README.md#L3" rel="noopener noreferrer"&gt;cmd/ate-setup/README.md#L3&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/dataplane/envoy/README.md" rel="noopener noreferrer"&gt;cmd/dataplane/envoy/README.md&lt;/a&gt;.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource ownership and assignment
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj72ertcixg9iimgs8mrs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj72ertcixg9iimgs8mrs.jpg" alt="Agent Substrate between the agent application layer and Kubernetes, with sandbox isolation and storage beneath it." width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/bringing-you-agent-sandbox-on-gke-and-agent-substrate" rel="noopener noreferrer"&gt;Bringing you Agent Sandbox on GKE and Agent Substrate, Google Cloud Blog (May 20, 2026)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The green sandbox-isolation block is the foundation for the checkpoint and restore operations. Agent Substrate incorporates Agent Sandbox's secure runtime and snapshot capabilities into a separate execution layer.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;classDiagram
    namespace kube-apiserver {
        class WorkerPool {
            &amp;lt;&amp;lt;CRD&amp;gt;&amp;gt;
        }
        class Deployment
        class WorkerPod {
            ateom
            runsc
        }
    }

    namespace ate-api-server {
        class ActorTemplate {
            &amp;lt;&amp;lt;record&amp;gt;&amp;gt;
        }
        class Actor {
            &amp;lt;&amp;lt;record&amp;gt;&amp;gt;
            status
            snapshotRefs
        }
        class Worker {
            &amp;lt;&amp;lt;record&amp;gt;&amp;gt;
            actorAssignments
            podIP
        }
    }

    ActorTemplate ..&amp;gt; WorkerPool : worker_selector (labels)
    WorkerPool ..&amp;gt; Deployment : reconciled by atecontroller
    Deployment "1" *-- "*" WorkerPod : manages
    Actor "*" --&amp;gt; "1" ActorTemplate : actor_template
    Actor "0..*" --&amp;gt; "0..1" Worker : runs on
    Worker "1" --&amp;gt; "1" WorkerPod : maps to&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Adapted from &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L268-L303" rel="noopener noreferrer"&gt;docs/architecture.md#L268-L303&lt;/a&gt;; the multiplicity reflects the implementation's support for multiple Actors per Worker.&lt;/p&gt;

&lt;p&gt;The split between API servers is central. Infrastructure resources stay in Kubernetes, while frequent Actor/Worker assignment changes go through &lt;code&gt;ate-api-server&lt;/code&gt; and its state store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two resource layers
&lt;/h3&gt;

&lt;p&gt;The architecture gives three reasons for this split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep high-frequency Actor updates away from the Kubernetes control plane.&lt;/li&gt;
&lt;li&gt;Use low-latency lookups and atomic assignments for resumption. The document's 100ms figure is a design target, not a result measured here.&lt;/li&gt;
&lt;li&gt;Retain Kubernetes RBAC, auditing and policies for infrastructure resources such as WorkerPools and SandboxConfigs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail in that document is stale: it says authorization has not been implemented. The same v0.5.0 tag contains OpenFGA-based authorization. &lt;code&gt;--experimental-enable-authz&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt; and controls enforcement for ordinary Control RPCs. AccessPolicy RPCs are checked regardless of that flag.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L247-L261" rel="noopener noreferrer"&gt;docs/architecture.md#L247-L261&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/main.go#L84-L88" rel="noopener noreferrer"&gt;cmd/ateapi/main.go#L84-L88&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/internal/authz/interceptor.go#L28-L60" rel="noopener noreferrer"&gt;cmd/ateapi/internal/authz/interceptor.go#L28-L60&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ate-api-server: the Actor control plane
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ateapi&lt;/code&gt; owns Actor lifecycles, schedules them onto Workers and coordinates snapshots. Its gRPC server connects to PostgreSQL, creates Kubernetes clients and configures its mTLS credentials.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/glossary.md#L56-L58" rel="noopener noreferrer"&gt;docs/glossary.md#L56-L58&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/main.go#L509-L521" rel="noopener noreferrer"&gt;cmd/ateapi/main.go#L509-L521&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/main.go#L560-L586" rel="noopener noreferrer"&gt;cmd/ateapi/main.go#L560-L586&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;templateReconciler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;controlapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewActorTemplateReconciler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persistence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;controlSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;templateResyncInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;templateReconciler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdownCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;workerAssignmentReconciler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;controlapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWorkerAssignmentReconciler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persistence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workerCache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;workerAssignmentReconciler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdownCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/main.go#L326-L330" rel="noopener noreferrer"&gt;cmd/ateapi/main.go#L326-L330&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Despite their names, these reconcilers are internal Substrate loops rather than controller-runtime reconcilers watching custom resources. &lt;code&gt;ActorTemplateReconciler&lt;/code&gt; prepares golden snapshots; &lt;code&gt;WorkerAssignmentReconciler&lt;/code&gt; handles Actors lost when &lt;code&gt;ateom&lt;/code&gt; restarts.&lt;/p&gt;

&lt;p&gt;This component does not map one-to-one to kube-apiserver: it combines Actor-specific API handling, scheduling and workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  atecontroller: conventional Kubernetes reconciliation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;atecontroller&lt;/code&gt; uses &lt;code&gt;ctrl.NewManager&lt;/code&gt; and registers four reconcilers, including &lt;code&gt;WorkerPoolReconciler&lt;/code&gt;. Its core structure is familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WorkerPoolReconciler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;wp&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;atev1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkerPool&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NamespacedName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k8errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to get worker pool %q: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NamespacedName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDeletionTimestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WorkerPool is being deleted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconcileWorkerPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Failed to reconcile worker pool"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/atecontroller/main.go#L233-L276" rel="noopener noreferrer"&gt;cmd/atecontroller/main.go#L233-L276&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/atecontroller/internal/controllers/workerpool_controller.go#L105-L129" rel="noopener noreferrer"&gt;cmd/atecontroller/internal/controllers/workerpool_controller.go#L105-L129&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Reconciliation does more than produce a Deployment. The WorkerPool reconciler protects referenced SandboxConfigs with finalizers; a separate &lt;code&gt;SandboxConfigProtectionReconciler&lt;/code&gt; releases protection. Updating the recorded SandboxConfig uses two status writes so that status never references a configuration whose protection has already been released.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Owns(&amp;amp;Deployment{})&lt;/code&gt; and &lt;code&gt;Watches(&amp;amp;SandboxConfig{}, ...)&lt;/code&gt; connect these resources through normal controller-runtime mechanisms.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/atecontroller/internal/controllers/workerpool_controller.go#L134-L136" rel="noopener noreferrer"&gt;cmd/atecontroller/internal/controllers/workerpool_controller.go#L134-L136&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/atecontroller/internal/controllers/workerpool_controller.go#L416-L425" rel="noopener noreferrer"&gt;cmd/atecontroller/internal/controllers/workerpool_controller.go#L416-L425&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  atelet and ateom: checkpoint and restore
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;atelet&lt;/code&gt; runs on each node as a DaemonSet. &lt;code&gt;ateom&lt;/code&gt; runs inside worker Pods and exposes operations such as &lt;code&gt;RunWorkload&lt;/code&gt;, &lt;code&gt;CheckpointWorkload&lt;/code&gt; and &lt;code&gt;RestoreWorkload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;atelet&lt;/code&gt; coordinates worker supervision, image caching and CSI volumes. It also reuses kubelet's image credential-provider mechanism through &lt;code&gt;--image-credential-provider-config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L324-L326" rel="noopener noreferrer"&gt;docs/architecture.md#L324-L326&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/atelet/main.go#L103-L104" rel="noopener noreferrer"&gt;cmd/atelet/main.go#L103-L104&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Snapshot upload and download are delegated to &lt;code&gt;objectstore-plugin&lt;/code&gt; sidecars over a Unix-socket gRPC API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin role&lt;/th&gt;
&lt;th&gt;Operations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NodeProvider&lt;/code&gt;, alongside &lt;code&gt;atelet&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Upload and fetch snapshot files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ControlProvider&lt;/code&gt;, alongside &lt;code&gt;ate-api-server&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Copy and delete whole snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The main processes do not directly transfer snapshots to object storage. This does not mean they hold no credentials at all: mTLS and database authentication remain separate concerns.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L330" rel="noopener noreferrer"&gt;docs/architecture.md#L330&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For gVisor, &lt;code&gt;cmd/ateom-gvisor/runsc.go&lt;/code&gt; invokes &lt;code&gt;runsc checkpoint&lt;/code&gt; and &lt;code&gt;runsc restore&lt;/code&gt;. The microVM implementation uses Kata integration and Cloud Hypervisor, with &lt;code&gt;userfaultfd&lt;/code&gt; demand paging when restoring memory.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L328-L338" rel="noopener noreferrer"&gt;docs/architecture.md#L328-L338&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kubelet and the container runtime still run the worker Pod itself. Substrate manages sandboxed Actors inside it. Kubernetes already has a &lt;a href="https://kubernetes.io/docs/reference/node/kubelet-checkpoint-api/" rel="noopener noreferrer"&gt;Kubelet Checkpoint API&lt;/a&gt;; Substrate's distinction is coordinating persistence, assignment, restore and reconnection as an Actor lifecycle.&lt;/p&gt;
&lt;h3&gt;
  
  
  atenet: routing that can resume its destination
&lt;/h3&gt;

&lt;p&gt;Ingress requests identify an Actor with the &lt;code&gt;ate-target-actor&lt;/code&gt; header, formatted as &lt;code&gt;&amp;lt;atespace&amp;gt;/&amp;lt;actor&amp;gt;&lt;/code&gt;. In the Envoy configuration described here, an &lt;code&gt;ext_proc&lt;/code&gt; processor calls the control plane's &lt;code&gt;ResumeActor&lt;/code&gt; RPC before resolving the destination.&lt;/p&gt;

&lt;p&gt;Once the assignment is available, the router opens an mTLS &lt;code&gt;CONNECT&lt;/code&gt; tunnel to the Worker's &lt;code&gt;atunnel&lt;/code&gt; listener on port 8443. &lt;code&gt;atunnel&lt;/code&gt;, hosted by &lt;code&gt;ateom&lt;/code&gt;, forwards traffic over the Actor's private veth interface. Worker Pod port 80 is not a direct Actor ingress endpoint.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L344-L354" rel="noopener noreferrer"&gt;docs/architecture.md#L344-L354&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Kubernetes analogy is Service/Ingress/Gateway routing with an extra orchestration step: restore the destination if necessary before forwarding to it.&lt;/p&gt;
&lt;h3&gt;
  
  
  podcertcontroller: Pod certificate signers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;podcertcontroller&lt;/code&gt; provides three signers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;servicedns.podcert.ate.dev/identity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service DNS certificates backed by a local CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podidentity.podcert.ate.dev/identity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pod identity certificates equivalent to Kubernetes service account identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres.podcert.ate.dev/identity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL login certificates for &lt;code&gt;ate-api-server&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/podcertcontroller/main.go#L15-L22" rel="noopener noreferrer"&gt;cmd/podcertcontroller/main.go#L15-L22&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It watches &lt;code&gt;certificates.k8s.io/v1&lt;/code&gt; &lt;code&gt;PodCertificateRequest&lt;/code&gt; objects. The API became GA in Kubernetes v1.37:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;PodCertificateRequest&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1.34"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PreRelease&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featuregate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alpha&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1.35"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PreRelease&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featuregate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1.37"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PreRelease&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featuregate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GA&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c"&gt;// LockToDefault in 1.38&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/features/kube_features.go#L1945-L1949" rel="noopener noreferrer"&gt;kube_features.go&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The API lets kubelet request certificates and project them into Pods. A signer must still process those requests. The well-known &lt;code&gt;kubernetes.io/kube-apiserver-client-pod&lt;/code&gt; signer described in the API comments is unimplemented in this version.&lt;/p&gt;

&lt;p&gt;Substrate fills its own needs with the three signers above. Its README calls the controller a “polyfill” for the missing signers.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/api/certificates/v1/types.go#L437-L444" rel="noopener noreferrer"&gt;certificate API types&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/README.md#L268-L269" rel="noopener noreferrer"&gt;README.md#L268-L269&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keep session state and API credentials separate
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Isolation, identity and credentials are separate controls
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapfx8ha7h3pgrco8llbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapfx8ha7h3pgrco8llbg.png" alt="The agent environment combines identity-based ingress and egress, secrets management, kernel isolation and rapid suspend/resume." width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/agent-substrate-available-on-gke" rel="noopener noreferrer"&gt;Agent Substrate on GKE, Google Cloud Blog (September 16, 2026)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kernel isolation does not by itself protect external API credentials. That also requires the gateway and credential-provider configuration described below.&lt;/p&gt;

&lt;p&gt;The security document distinguishes &lt;strong&gt;Met&lt;/strong&gt;, &lt;strong&gt;In Progress&lt;/strong&gt;, &lt;strong&gt;Future&lt;/strong&gt; and &lt;strong&gt;Out of Scope&lt;/strong&gt;. Selected principles in v0.5.0 are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Documented status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Always Sandboxed&lt;/td&gt;
&lt;td&gt;Run Actors in gVisor or Cloud Hypervisor microVMs rather than relying on ordinary Linux containers alone&lt;/td&gt;
&lt;td&gt;Met&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero Ambient Credentials&lt;/td&gt;
&lt;td&gt;Do not implicitly give Actors infrastructure permissions or external API keys&lt;/td&gt;
&lt;td&gt;Met; distinct from demos that explicitly pass keys in environment variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No Access by Default&lt;/td&gt;
&lt;td&gt;Restrict Actor access to the control plane, separate from required infrastructure traffic&lt;/td&gt;
&lt;td&gt;Loopback/private netns: Met; DNS filtering: In Progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail-Closed Policy-Enforced Egress Gateway&lt;/td&gt;
&lt;td&gt;Control outbound traffic through an egress proxy&lt;/td&gt;
&lt;td&gt;Gateway: Met; fail-closed behavior: In Progress, with UDP passthrough noted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/security.md#L30-L33" rel="noopener noreferrer"&gt;docs/security.md#L30-L33&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/security.md#L41" rel="noopener noreferrer"&gt;docs/security.md#L41&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/security.md#L59" rel="noopener noreferrer"&gt;docs/security.md#L59&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/security.md#L75-L77" rel="noopener noreferrer"&gt;docs/security.md#L75-L77&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The same document lists Atespace authorization as In Progress, and node blast-radius containment and storage encryption as Future. The security architecture is not evidence that every control is fully implemented. Ordinary Control RPC authorization still requires the experimental flag.&lt;/p&gt;
&lt;h3&gt;
  
  
  Inject credentials outside the Actor
&lt;/h3&gt;

&lt;p&gt;With egress credential injection, an Actor sends an HTTPS request containing a placeholder header. The gateway obtains the real credential and substitutes it outside the sandbox:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Actor sends the request with a placeholder value.&lt;/li&gt;
&lt;li&gt;The egress gateway terminates TLS and matches an &lt;code&gt;EgressPolicy&lt;/code&gt; header-replacement rule.&lt;/li&gt;
&lt;li&gt;The credential provider authorizes access and reads the secret.&lt;/li&gt;
&lt;li&gt;The gateway replaces the header and makes a separate TLS connection to the destination.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Actor must trust the gateway's CA. If the request does not contain the header to replace, credential lookup and replacement do not occur.&lt;/p&gt;

&lt;p&gt;This keeps the API key outside Actor memory and snapshots. It does not prevent misuse of an allowed API; destination restrictions and application authorization still matter.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/egress-credential-injection.md#L1-L15" rel="noopener noreferrer"&gt;docs/egress-credential-injection.md#L1-L15&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/egress-credential-injection.md#L41-L54" rel="noopener noreferrer"&gt;docs/egress-credential-injection.md#L41-L54&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The reference &lt;code&gt;kubernetes-secrets&lt;/code&gt; provider starts with an empty deny-all policy. Readable namespaces must be explicitly allowed for each Atespace. Actors do not read Kubernetes Secrets themselves.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/README.md#L132-L136" rel="noopener noreferrer"&gt;README.md#L132-L136&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  EgressPolicy defines destinations and header replacement
&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;EgressPolicy&lt;/code&gt; specifies allowed traffic and header replacement rules. The credential provider separately decides whether the Actor can use the referenced secret. The API defines the policy as an unordered set of allows: traffic matching no rule is denied.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/pkg/proto/ateapipb/ateapi.proto#L609-L610" rel="noopener noreferrer"&gt;pkg/proto/ateapipb/ateapi.proto#L609-L610&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, allow HTTPS to &lt;code&gt;api.example.com&lt;/code&gt; and replace its &lt;code&gt;Authorization&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;hostnames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api.example.com"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;effects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;replaceHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authorization&lt;/span&gt;
        &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="na"&gt;credentialUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ate-secret://k8s.io/default/ns1/example-api/token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/egress-credential-injection.md#L69-L76" rel="noopener noreferrer"&gt;docs/egress-credential-injection.md#L69-L76&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The policy is associated with an Actor rather than embedded in its ActorTemplate:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create an egress policy.&lt;/span&gt;
kubectl ate create egress-policy &amp;lt;actor-name&amp;gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;atespace&amp;gt; &lt;span class="nt"&gt;-f&lt;/span&gt; policy.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/kubectl-ate/README.md#L247-L253" rel="noopener noreferrer"&gt;cmd/kubectl-ate/README.md#L247-L253&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Claude Code demo discussed later directly places &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; in the ActorTemplate environment. That differs from gateway injection. In either case, the Actor needs an egress policy permitting its HTTPS destination, such as &lt;code&gt;api.anthropic.com&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  The complete request path
&lt;/h3&gt;

&lt;p&gt;With routing and credential injection in place, an HTTP-triggered agent can resume and call an external LLM as follows. This combines the documented flows; Counter itself does not call an LLM.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    actor User as User / higher-level system
    participant Router as atenet-router
    participant API as ate-api-server
    participant Atelet as atelet
    participant Tunnel as ateom / atunnel
    participant Agent as Actor
    participant Egress as atenet-egress
    participant Cred as credential provider
    participant LLM as External LLM API

    User-&amp;gt;&amp;gt;Router: HTTP request (ate-target-actor)
    Router-&amp;gt;&amp;gt;API: ResumeActor
    alt Actor is suspended
        API-&amp;gt;&amp;gt;Atelet: Restore request
        Atelet-&amp;gt;&amp;gt;Tunnel: RestoreWorkload
        Tunnel--&amp;gt;&amp;gt;Atelet: Restore complete
        Atelet--&amp;gt;&amp;gt;API: Complete
    else Actor is running
        Note over API: Return existing assignment
    end
    API--&amp;gt;&amp;gt;Router: Worker assignment
    Router-&amp;gt;&amp;gt;Tunnel: Forward through mTLS CONNECT
    Tunnel-&amp;gt;&amp;gt;Agent: Forward request
    Agent-&amp;gt;&amp;gt;Egress: HTTPS (placeholder header)
    Egress-&amp;gt;&amp;gt;Cred: Request credential
    Cred--&amp;gt;&amp;gt;Egress: Return API key after authorization
    Egress-&amp;gt;&amp;gt;LLM: Replace header and send over a separate TLS connection
    LLM--&amp;gt;&amp;gt;Egress: Response
    Egress--&amp;gt;&amp;gt;Agent: Response
    Agent--&amp;gt;&amp;gt;Tunnel: Result
    Tunnel--&amp;gt;&amp;gt;Router: Response
    Router--&amp;gt;&amp;gt;User: Response
    Note over User,API: A higher-level system can request SuspendActor to save state and release the assignment&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md#L388-L413" rel="noopener noreferrer"&gt;Resume sequence&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/egress-credential-injection.md#L30-L54" rel="noopener noreferrer"&gt;credential injection&lt;/a&gt;. Cold boot without a snapshot and other branches are omitted.&lt;/p&gt;

&lt;p&gt;The Actor-to-gateway and gateway-to-provider connections use separate TLS sessions. The gateway must terminate the first to replace headers in the second.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try the Counter demo with kind
&lt;/h2&gt;

&lt;p&gt;The Counter workload makes state preservation visible: each request increments counters in process memory, a file and the root filesystem. The example below uses gVisor and includes recorded output from a local kind run.&lt;/p&gt;
&lt;h3&gt;
  
  
  Set up the cluster and demo
&lt;/h3&gt;

&lt;p&gt;Building v0.5.0 requires Go 1.27.0 and a working Docker-based development environment. The microVM option additionally requires &lt;code&gt;/dev/kvm&lt;/code&gt; and suitable virtualization support.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/go.mod#L1-L5" rel="noopener noreferrer"&gt;go.mod#L1-L5&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/hack/create-kind-cluster.sh#L74-L87" rel="noopener noreferrer"&gt;hack/create-kind-cluster.sh#L74-L87&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use a dedicated checkout and cluster name. The creation script deletes and recreates an existing cluster with the same name, so choose one that does not overlap with an environment you want to keep.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v0.5.0 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/agent-substrate/substrate.git substrate-v0.5.0
&lt;span class="nb"&gt;cd &lt;/span&gt;substrate-v0.5.0
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KIND_CLUSTER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ate-overview

&lt;span class="c"&gt;# create cluster and local registry (IPv4; IP_FAMILY=dual|ipv6 overrides)&lt;/span&gt;
hack/create-kind-cluster.sh

&lt;span class="c"&gt;# install ate, PostgreSQL, rustfs&lt;/span&gt;
hack/install-ate-kind.sh &lt;span class="nt"&gt;--deploy-ate-system&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--credential-provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"name":"k8s.io"}'&lt;/span&gt;

&lt;span class="c"&gt;# install counter demo&lt;/span&gt;
hack/install-ate-kind.sh &lt;span class="nt"&gt;--deploy-demo-counter&lt;/span&gt;

&lt;span class="c"&gt;# install kubectl-ate&lt;/span&gt;
go &lt;span class="nb"&gt;install&lt;/span&gt; ./cmd/kubectl-ate

&lt;span class="c"&gt;# create a counter actor in the demo's atespace (--template names the&lt;/span&gt;
&lt;span class="c"&gt;# actor template, resolved in the actor's atespace)&lt;/span&gt;
kubectl ate create actor my-counter-1 &lt;span class="nt"&gt;-a&lt;/span&gt; ate-demo-counter &lt;span class="nt"&gt;--template&lt;/span&gt; counter

&lt;span class="c"&gt;# port-forward the network router to bind to local port `8000`&lt;/span&gt;
kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; ate-system svc/atenet-router 8000:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/README.md#L111-L129" rel="noopener noreferrer"&gt;README.md#L111-L129&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The creation script sets up a local registry and a kind cluster with &lt;code&gt;kindest/node:v1.37.0&lt;/code&gt;. It explicitly enables certificate-related feature gates, including &lt;code&gt;ClusterTrustBundleProjection&lt;/code&gt;; some comments still describe v1.36 requirements.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/hack/create-kind-cluster.sh#L104-L113" rel="noopener noreferrer"&gt;hack/create-kind-cluster.sh#L104-L113&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The system installation uses &lt;code&gt;ko&lt;/code&gt; to build and push its Go components and deploys PostgreSQL, RustFS, CRDs and SandboxConfig resources. The Counter installation adds its WorkerPool and ActorTemplate and waits for a golden snapshot.&lt;/p&gt;
&lt;h3&gt;
  
  
  WorkerPool and ActorTemplate
&lt;/h3&gt;

&lt;p&gt;The demo's WorkerPool includes the following settings. The installer resolves template placeholders and &lt;code&gt;ko://&lt;/code&gt; references before applying the configuration.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ate.dev/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WorkerPool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;counter&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ate-demo-counter&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;counter&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;workerImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor&lt;/span&gt;
  &lt;span class="na"&gt;sandboxClasses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gvisor&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;250m&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/counter/counter.yaml.tmpl#L26-L63" rel="noopener noreferrer"&gt;demos/counter/counter.yaml.tmpl#L26-L63&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An ActorTemplate is not a Kubernetes object. This YAML maps to Substrate's protobuf fields and is passed through &lt;code&gt;kubectl ate create actor-template&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;atespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ate-demo-counter&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;counter&lt;/span&gt;
&lt;span class="na"&gt;workerSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;counter&lt;/span&gt;
&lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;counter&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ko://github.com/agent-substrate/substrate/demos/counter&lt;/span&gt;
  &lt;span class="na"&gt;wakeupProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/readyz&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
    &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/counter&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memory&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
&lt;span class="na"&gt;snapshotConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;preferredFidelity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SNAPSHOT_FIDELITY_MEMORY&lt;/span&gt;
  &lt;span class="na"&gt;storageLocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gs://${BUCKET_NAME}/ate-demo-counter/&lt;/span&gt;
&lt;span class="na"&gt;sandboxConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sandboxClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gvisor&lt;/span&gt;
  &lt;span class="na"&gt;configName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gvisor-default&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
  &lt;span class="na"&gt;durableDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/counter/counter-template.yaml.tmpl#L22-L69" rel="noopener noreferrer"&gt;demos/counter/counter-template.yaml.tmpl#L22-L69&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SNAPSHOT_FIDELITY_MEMORY&lt;/code&gt; requests preservation of process memory. &lt;code&gt;gvisor-default&lt;/code&gt; names a cluster-scoped SandboxConfig whose assets specify where to download the runtime binaries:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ate.dev/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SandboxConfig&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gvisor-default&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sandboxconfig.ate.dev/is-class-default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sandboxClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gvisor&lt;/span&gt;
  &lt;span class="na"&gt;defaultVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3&lt;/span&gt;
  &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v3&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enabled&lt;/span&gt;
    &lt;span class="na"&gt;pauseImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry.k8s.io/pause:3.10.2@sha256:f548e0e8e3dc1896ca956272154dde3314e8cc4fde0a57577ee9fa1c63f5baf4"&lt;/span&gt;
    &lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;arm64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;gvisor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gs://gvisor/releases/nightly/2026-10-10/aarch64/gvisor.tar.zstd"&lt;/span&gt;
          &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aaf414f06eea7960238e43165c691f0a36cefc0593fb141e011e96211c7b6f57"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/manifests/ate-install/sandboxconfig-gvisor.yaml#L22-L48" rel="noopener noreferrer"&gt;manifests/ate-install/sandboxconfig-gvisor.yaml#L22-L48&lt;/a&gt;. Only the arm64 assets are shown; amd64 assets are omitted.&lt;/p&gt;
&lt;h3&gt;
  
  
  Observe Actor state and preserved counters
&lt;/h3&gt;

&lt;p&gt;A newly created Counter Actor starts suspended: its snapshot exists, but it has no Worker assignment yet.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl ate create actor my-counter-1 &lt;span class="nt"&gt;-a&lt;/span&gt; ate-demo-counter &lt;span class="nt"&gt;--template&lt;/span&gt; counter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Recorded output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ATESPACE           NAME           TEMPLATE                   STATE                   WORKER POD   WORKER IPS   VERSION   AGE
ate-demo-counter   my-counter-1   ate-demo-counter/counter   ACTOR_STATE_SUSPENDED   &amp;lt;none&amp;gt;                    1         0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Send a request through the router. It resumes the Actor before forwarding, and the recorded response is HTTP 200:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"ate-target-actor: ate-demo-counter/my-counter-1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Recorded output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Length: 111
Content-Type: text/plain; charset=utf-8

hello from: 169.254.17.2 | preserved memory count: 1 | preserved file counter: 1 | preserved rootfs counter: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A second POST increments the counter to 2. The Actor is now running and assigned to a worker Pod:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"ate-target-actor: ate-demo-counter/my-counter-1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl ate get actor my-counter-1 &lt;span class="nt"&gt;-a&lt;/span&gt; ate-demo-counter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Recorded output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ATESPACE           NAME           TEMPLATE                   STATE                 WORKER POD                                  WORKER IPS    VERSION   AGE
ate-demo-counter   my-counter-1   ate-demo-counter/counter   ACTOR_STATE_RUNNING   ate-demo-counter/counter-796d677995-v55tv   10.244.0.23   3         20s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Explicitly suspend it, then send another request:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl ate &lt;span class="nb"&gt;suspend &lt;/span&gt;actor my-counter-1 &lt;span class="nt"&gt;-a&lt;/span&gt; ate-demo-counter
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"ate-target-actor: ate-demo-counter/my-counter-1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Recorded output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello from: 169.254.17.2 | preserved memory count: 3 | preserved file counter: 3 | preserved rootfs counter: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The memory, file and root-filesystem counters continue at 3. This illustrates the documented snapshot-and-resume lifecycle, including preserved process memory.&lt;/p&gt;

&lt;p&gt;Stop the port-forward when finished, then remove the dedicated cluster. The &lt;code&gt;kind-registry&lt;/code&gt; container is separate and remains after cluster deletion.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hack/kind.sh delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; ate-overview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Plan the deployment around more than Pods
&lt;/h2&gt;

&lt;p&gt;Worker Pods and CRDs are only part of the deployment. The following table distinguishes infrastructure that must be supplied from components the installer can deploy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dependency&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Installation considerations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Actor/Worker state store&lt;/td&gt;
&lt;td&gt;The kind setup includes a development database; production operators supply database identities, schema and connection settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCS or S3-compatible object storage&lt;/td&gt;
&lt;td&gt;Durable snapshot storage&lt;/td&gt;
&lt;td&gt;Configure &lt;code&gt;storage.bucketName&lt;/code&gt; / &lt;code&gt;BUCKET_NAME&lt;/code&gt;; kind uses &lt;code&gt;ate-snapshots&lt;/code&gt; and an in-cluster RustFS service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress credential provider&lt;/td&gt;
&lt;td&gt;Resolve credentials outside Actors&lt;/td&gt;
&lt;td&gt;Provider selection is required, including explicit &lt;code&gt;{"enabled":false}&lt;/code&gt; to disable injection; &lt;code&gt;{"name":"k8s.io"}&lt;/code&gt; deploys the reference provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox runtime binaries&lt;/td&gt;
&lt;td&gt;Run Actors with gVisor or microVMs&lt;/td&gt;
&lt;td&gt;Selected through a cluster-scoped SandboxConfig; default gVisor assets come from a public GCS bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy dataplane image&lt;/td&gt;
&lt;td&gt;Router and egress proxies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--atenet-dataplane&lt;/code&gt; selects Envoy, requiring its custom build, or agentgateway, using a published image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object-storage permissions&lt;/td&gt;
&lt;td&gt;Let plugin sidecars transfer snapshots&lt;/td&gt;
&lt;td&gt;For GCS, configure the required Workload Identity/IAM bindings; the kind substitute uses local credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSI driver, when needed&lt;/td&gt;
&lt;td&gt;Attach CSI-backed persistent storage to Actors&lt;/td&gt;
&lt;td&gt;Not needed for the demo's &lt;code&gt;durableDir&lt;/code&gt;; Substrate imposes additional controller/node deployment requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;External databases are not provisioned by &lt;code&gt;ateapi&lt;/code&gt; or &lt;code&gt;ate-setup&lt;/code&gt;. Snapshot-bucket selection and credential-provider selection also need explicit configuration, even when other defaults are available.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/postgres.md#L8" rel="noopener noreferrer"&gt;docs/postgres.md#L8&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/operator-install.md#L314" rel="noopener noreferrer"&gt;docs/operator-install.md#L314&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/egress-credential-injection.md#L128-L131" rel="noopener noreferrer"&gt;docs/egress-credential-injection.md#L128-L131&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Missing GCS IAM bindings can produce a &lt;code&gt;403&lt;/code&gt; on the first suspend/resume, and also prevent control-plane snapshot operations and image pulls.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/tools/setup-gcp/README.md#L154-L159" rel="noopener noreferrer"&gt;tools/setup-gcp/README.md#L154-L159&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Substrate directly orchestrates CSI operations from &lt;code&gt;ateapi&lt;/code&gt; and &lt;code&gt;atelet&lt;/code&gt;. A CSI controller must be reachable as a cluster-network gRPC service, unlike the usual controller-side Unix socket arrangement. See &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/csi-deployment.md#L5" rel="noopener noreferrer"&gt;docs/csi-deployment.md#L5&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the Kubernetes v1.37 setup covered here, Pod certificate and ClusterTrustBundle features are GA and enabled by default. Substrate also includes its certificate controller. Provider-specific installation requirements should still be checked separately.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/features/kube_features.go#L1417-L1421" rel="noopener noreferrer"&gt;ClusterTrustBundle feature definition&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  GKE availability
&lt;/h3&gt;

&lt;p&gt;Google announced Agent Substrate availability on GKE on September 16, 2026.&lt;/p&gt;

&lt;p&gt;The GKE offering installs open-source Substrate onto &lt;strong&gt;GKE Standard&lt;/strong&gt;; Autopilot is not supported. Evaluation and non-production use are available, while production support is allowlisted under a private GA program. Substrate has no additional product charge, but the GKE and other resources created still incur their normal charges.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://docs.cloud.google.com/kubernetes-engine/ai-ml/install-overview-substrate" rel="noopener noreferrer"&gt;Install Agent Substrate on GKE&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The announcement also describes ComputeClasses and optional Filestore agent volumes. Shared workspace storage serves a different purpose from snapshots of execution state.&lt;/p&gt;

&lt;p&gt;Google reports sub-500ms resumption, more than 500 suspend/resume activations per second and 10x density. These are &lt;strong&gt;Google's published figures&lt;/strong&gt;, separate from upstream design targets and the Counter example.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/agent-substrate-available-on-gke" rel="noopener noreferrer"&gt;Agent Substrate on GKE, Google Cloud Blog (September 16, 2026)&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to evaluate in your own workload
&lt;/h2&gt;

&lt;p&gt;Substrate is worth examining when a fleet has many isolated, stateful environments but only a fraction need to execute at once. The useful question is how much resident capacity suspend/resume can release, and whether restoration meets the application's latency needs.&lt;/p&gt;

&lt;p&gt;Start with three checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suspension boundaries:&lt;/strong&gt; identify when an environment can safely stop, including outstanding network calls and application work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State requirements:&lt;/strong&gt; decide what must survive, then select snapshot fidelity and any persistent workspace storage accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational ownership:&lt;/strong&gt; account for the database, snapshot backend, identity and egress services as well as the Workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Counter demo exposes the core mechanism. An agent platform must then connect it to real session events and measure its own activation latency and capacity use.&lt;/p&gt;
&lt;h2&gt;
  
  
  Further implementation details
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;
  Claude Code demo: workload execution versus UI state
  &lt;h3&gt;
  
  
  What the Claude Code demo actually implements
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;claude-code-multiplex&lt;/code&gt; defines three Claude Code Actors—&lt;code&gt;luna&lt;/code&gt;, &lt;code&gt;mars&lt;/code&gt; and &lt;code&gt;orion&lt;/code&gt;—with a two-Pod WorkerPool. It uses the control API rather than sending workload HTTP through &lt;code&gt;atenet-router&lt;/code&gt;, so it does not use the routing header.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/claude-code-multiplex/README.md#L14-L21" rel="noopener noreferrer"&gt;demos/claude-code-multiplex/README.md#L14-L21&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An ActorTemplate supplies the task and interval as environment variables:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ACTOR_NAME&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;luna"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TASK&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tell&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;me&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;short,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;surprising&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fact&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;about&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Moon.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sentence."&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;INTERVAL_SECONDS&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;45"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ANTHROPIC_API_KEY&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ANTHROPIC_API_KEY}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/claude-code-multiplex/agent-luna-template.yaml.tmpl#L34-L42" rel="noopener noreferrer"&gt;demos/claude-code-multiplex/agent-luna-template.yaml.tmpl#L34-L42&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The workload repeatedly runs &lt;code&gt;claude --print&lt;/code&gt; and then sleeps:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  ...
  claude &lt;span class="nt"&gt;--print&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TASK&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[demo-actor:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACTOR_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] claude exited non-zero"&lt;/span&gt;
  ...
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INTERVAL_SECONDS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/claude-code-multiplex/workload/run.sh#L17-L49" rel="noopener noreferrer"&gt;demos/claude-code-multiplex/workload/run.sh#L17-L49&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That idle interval is an opportunity for multiplexing. However, the v0.5.0 UI does not implement everything its README's task-flow description suggests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;UI behavior&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;“Give a task”&lt;/td&gt;
&lt;td&gt;Records the selected Actor and task in UI-server assignment state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;queued&lt;/code&gt; / &lt;code&gt;running&lt;/code&gt; / &lt;code&gt;completed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Derived from timestamps and randomized waiting/execution durations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actor state and logs&lt;/td&gt;
&lt;td&gt;Retrieved from the Substrate and Kubernetes APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;giveTask&lt;/code&gt; does not send a task to the Actor or call &lt;code&gt;ResumeActor&lt;/code&gt; / &lt;code&gt;SuspendActor&lt;/code&gt;. A comment in &lt;code&gt;computeState&lt;/code&gt; identifies the task state as a time-based UI display rather than Substrate state.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/claude-code-multiplex/ui/server.go#L252-L315" rel="noopener noreferrer"&gt;demos/claude-code-multiplex/ui/server.go#L252-L315&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A changing UI status therefore does not establish that a real LLM task completed or automatic multiplexing occurred. The useful pieces are the ActorTemplates, the periodic Claude Code workload and the state-viewing UI. Verify suspend/resume through real API operations and Actor state, as in the Counter demo.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  RustFS: the local snapshot backend
  &lt;h3&gt;
  
  
  RustFS
&lt;/h3&gt;

&lt;p&gt;RustFS is a separate, Rust-based S3-compatible object store. The kind installation uses it as its local snapshot backend. The v0.5.0 manifest selects &lt;code&gt;rustfs/rustfs:1.0.0-beta.3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This development setup does not replace production storage availability, persistence or operational design. &lt;code&gt;objectstore-plugin&lt;/code&gt; uses RustFS through its S3 backend; GCS uses the same plugin interface with a different backend.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/manifests/ate-install/kind/rustfs.yaml#L55-L75" rel="noopener noreferrer"&gt;manifests/ate-install/kind/rustfs.yaml#L55-L75&lt;/a&gt;, &lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;rustfs/rustfs&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  ReBAC: deriving permissions from relationships
  &lt;h3&gt;
  
  
  ReBAC and Kubernetes RBAC
&lt;/h3&gt;

&lt;p&gt;Substrate's API authorization uses OpenFGA's relationship-based access control model. In this version, ordinary Control RPC enforcement remains experimental and disabled by default.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/security.md#L46" rel="noopener noreferrer"&gt;docs/security.md#L46&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kubernetes RBAC checks applicable RoleBindings and ClusterRoleBindings against Role/ClusterRole rules: API groups, resources, verbs, namespaces and sometimes resource names. It does not automatically derive access from ownership or arbitrary parent-child relationships.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" rel="noopener noreferrer"&gt;Using RBAC Authorization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ReBAC derives access from relations between subjects and objects, and potentially relations between objects. Inheritance is explicit in the authorization model, rather than automatic for every parent-child association.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://openfga.dev/docs/modeling/getting-started" rel="noopener noreferrer"&gt;OpenFGA: Modeling—Getting Started&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Substrate's &lt;code&gt;.fga&lt;/code&gt; model makes a Worker's read permission depend on its parent &lt;code&gt;global&lt;/code&gt; object's &lt;code&gt;viewer&lt;/code&gt; or &lt;code&gt;controller&lt;/code&gt; relation:&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;
  &lt;span class="n"&gt;relations&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;parent_global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;host_node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;can_get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;parent_global&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;parent_global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Source: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/internal/authz/model.fga#L29-L34" rel="noopener noreferrer"&gt;cmd/ateapi/internal/authz/model.fga#L29-L34&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An Atespace can inherit its owner from its parent global object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;atespace&lt;/span&gt;
  &lt;span class="n"&gt;relations&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;parent_global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;parent_global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An ActorTemplate can derive read permission from the viewer relation on its parent Atespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="n"&gt;actor_template&lt;/span&gt;
  &lt;span class="n"&gt;relations&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;parent_atespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;atespace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;can_get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;parent_atespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sources: &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/internal/authz/model.fga#L71-L77" rel="noopener noreferrer"&gt;cmd/ateapi/internal/authz/model.fga#L71-L77&lt;/a&gt;, &lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/cmd/ateapi/internal/authz/model.fga#L101-L106" rel="noopener noreferrer"&gt;cmd/ateapi/internal/authz/model.fga#L101-L106&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The relation data connects &lt;code&gt;global → atespace → actor_template&lt;/code&gt; and &lt;code&gt;global → worker&lt;/code&gt;. Permission checks follow those modelled relations. The practical difference from ordinary Kubernetes RoleBindings is the ability to derive child access from parent relations according to an explicit model.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Components and versions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent Substrate&lt;/td&gt;
&lt;td&gt;v0.5.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/agent-substrate/substrate" rel="noopener noreferrer"&gt;agent-substrate/substrate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Implementation, configuration and source examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;v1.37.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes/kubernetes" rel="noopener noreferrer"&gt;kubernetes/kubernetes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pod certificate APIs and feature gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kind&lt;/td&gt;
&lt;td&gt;v0.33.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes-sigs/kind" rel="noopener noreferrer"&gt;kubernetes-sigs/kind&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Local example using &lt;code&gt;kindest/node:v1.37.0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GKE availability information reflects the official documentation checked on October 20, 2026. The GKE installer does not necessarily distribute the same version as the upstream tag used here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Agent Substrate is pre-1.0 and does not guarantee backward compatibility. Unless another repository is named, source links in this article refer to &lt;code&gt;agent-substrate/substrate&lt;/code&gt; v0.5.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetespodcast.com/episode/272-agent-substrate/" rel="noopener noreferrer"&gt;Kubernetes Podcast #272: Agent Substrate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate" rel="noopener noreferrer"&gt;Agent Substrate repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/bringing-you-agent-sandbox-on-gke-and-agent-substrate" rel="noopener noreferrer"&gt;Bringing you Agent Sandbox on GKE and Agent Substrate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/agent-substrate-available-on-gke" rel="noopener noreferrer"&gt;Agent Substrate on GKE announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/kubernetes-engine/ai-ml/install-overview-substrate" rel="noopener noreferrer"&gt;Install Agent Substrate on GKE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/threat-model.md" rel="noopener noreferrer"&gt;Threat model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/glossary.md" rel="noopener noreferrer"&gt;Glossary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/security.md" rel="noopener noreferrer"&gt;Security guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/docs/egress-credential-injection.md" rel="noopener noreferrer"&gt;Egress credential injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate/blob/v0.5.0/demos/claude-code-multiplex/README.md" rel="noopener noreferrer"&gt;Claude Code multiplex demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes/enhancements/blob/8c2bd2ef10809249b4cec72145db9bb78430fb35/keps/sig-auth/4317-pod-certificates/README.md" rel="noopener noreferrer"&gt;Kubernetes Pod Certificates KEP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" rel="noopener noreferrer"&gt;Kubernetes RBAC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openfga.dev/docs/modeling/getting-started" rel="noopener noreferrer"&gt;OpenFGA modeling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rustfs/rustfs" rel="noopener noreferrer"&gt;RustFS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>ai</category>
      <category>opensource</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Kubernetes Memory on cgroup v2: Debugging OOM Kills and Evictions</title>
      <dc:creator>yosshi_</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:20:35 +0000</pubDate>
      <link>https://dev.to/yosshi_/kubernetes-memory-usage-and-cgroup-v2-telling-oom-kills-from-evictions-and-right-sizing-memory-1bi3</link>
      <guid>https://dev.to/yosshi_/kubernetes-memory-usage-and-cgroup-v2-telling-oom-kills-from-evictions-and-right-sizing-memory-1bi3</guid>
      <description>&lt;p&gt;A container is &lt;code&gt;OOMKilled&lt;/code&gt;, but &lt;code&gt;kubectl top&lt;/code&gt; never showed it reaching its limit. Or a Pod is evicted while your node dashboard still shows free memory. In both cases, the first question is which memory value the decision actually used.&lt;/p&gt;

&lt;p&gt;This guide separates cgroup-limit OOM, node-side OOM and kubelet eviction, then shows how to inspect the evidence and review memory requests and limits. It assumes familiarity with Pod resources and basic PromQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The working set is a heuristic, not a measurement of unreclaimable memory.&lt;/strong&gt; It subtracts &lt;code&gt;inactive_file&lt;/code&gt; from usage but still includes reclaimable active page cache and slab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguish memcg OOM from global OOM using kernel logs.&lt;/strong&gt; A container can be below its own limit when an ancestor cgroup reaches its limit or a node-side allocation fails. &lt;code&gt;OOMKilled&lt;/code&gt; and &lt;code&gt;SystemOOM&lt;/code&gt; alone cannot establish the cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction uses the kubelet's &lt;code&gt;memory.available&lt;/code&gt;, not Linux &lt;code&gt;MemAvailable&lt;/code&gt;.&lt;/strong&gt; Check the kubelet's statistics and configured thresholds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review requests and limits separately.&lt;/strong&gt; Requests influence placement and victim selection. To address a cgroup limit, reduce usage or adjust the limit after checking node capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check Memory QoS configuration as well as its feature gate.&lt;/strong&gt; In v1.37, the gate defaults to enabled, but request-based protection and &lt;code&gt;memory.high&lt;/code&gt; remain disabled by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article targets cgroup v2, Kubernetes v1.37.1 and containerd. See Components and versions for exact versions. The kind examples use a v1.37.0 node image.&lt;/p&gt;

&lt;p&gt;Use the investigation workflow for an incident, the metric map to understand a dashboard, or requests and limits for prevention. The kind examples explore reclaim and Memory QoS in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the failure before choosing a metric
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Evidence to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container termination reason is &lt;code&gt;OOMKilled&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The kernel killed a process; this alone does not distinguish the OOM source&lt;/td&gt;
&lt;td&gt;Kernel logs, container and ancestor cgroup limits, node allocation constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod is &lt;code&gt;Evicted&lt;/code&gt; with &lt;code&gt;The node was low on resource: memory.&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The kubelet responded to node memory pressure&lt;/td&gt;
&lt;td&gt;Kubelet &lt;code&gt;memory.available&lt;/code&gt;, configured thresholds and eviction message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit code is 137 without an OOM reason&lt;/td&gt;
&lt;td&gt;The process received SIGKILL; the exit code alone does not establish OOM&lt;/td&gt;
&lt;td&gt;Termination details, events and kernel logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;memcg OOM&lt;/strong&gt; means an allocation cannot fit within a cgroup limit after reclaim. &lt;strong&gt;Global OOM&lt;/strong&gt; means a node-side allocation cannot be satisfied after reclaim, potentially within NUMA or cpuset constraints. A container below its own limit can be affected by either an ancestor limit or a node-side failure.&lt;/p&gt;
&lt;h2&gt;
  
  
  Investigating OOM and eviction
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Investigation workflow
&lt;/h3&gt;

&lt;p&gt;Start with the observed termination or eviction, then compare metrics from the same time. Scrapes can miss the peak; kernel logs are stronger evidence of the OOM source than a sampled usage graph.&lt;/p&gt;
&lt;h4&gt;
  
  
  1. When a Pod was OOMKilled
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Check whether &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; is close to the limit. memcg OOM is evaluated against this value&lt;/li&gt;
&lt;li&gt;Compare with &lt;code&gt;container_memory_rss&lt;/code&gt; to tell whether anonymous pages are the main cause. An OOM can occur even when RSS is low&lt;/li&gt;
&lt;li&gt;If the gap between the working set or usage and RSS is large, page cache, &lt;code&gt;shmem&lt;/code&gt; or kernel memory may be pushing usage up&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;OOMKilled&lt;/code&gt; occurs despite headroom under the limit, suspect global OOM. Correlate &lt;code&gt;SystemOOM&lt;/code&gt; with kernel logs and inspect ancestor limits. A missing event does not establish memcg OOM&lt;/li&gt;
&lt;li&gt;If you see no spike in the metrics, suspect a surge shorter than the scrape interval. See My working set is below the limit, but the container was OOMKilled for what to check&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  2. When a Pod was evicted
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;If the message is &lt;code&gt;The node was low on resource: memory.&lt;/code&gt;, node-wide memory pressure is the cause&lt;/li&gt;
&lt;li&gt;Use the node-utilization query to check the kubelet-view node memory utilization. The node-exporter-based utilization alone is not enough to judge&lt;/li&gt;
&lt;li&gt;Check whether a Pod with &lt;strong&gt;a small (or unset) memory request relative to actual usage&lt;/strong&gt; is on the same node. Compare each Pod's &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; (cAdvisor) against &lt;code&gt;kube_pod_resource_request{resource="memory"}&lt;/code&gt; (kube-scheduler). Note that Pods with large requests make the scheduler keep placement density low, so they are unlikely to be the cause of pressure&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  3. To prevent recurrence
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Set a memory request informed by workload history to avoid excessive placement density. PromQL for checking is collected in Align memory request with actual usage
&lt;/li&gt;
&lt;li&gt;For containers where memcg OOM occurs, also review the limit&lt;/li&gt;
&lt;li&gt;Control placement with Pod anti-affinity or topology spread constraints so that Pods with large memory usage do not gather on the same node&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Telling which path occurred
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Using Kubernetes information
&lt;/h4&gt;

&lt;p&gt;Start by narrowing down with what you can check using &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where to look&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Last State&lt;/code&gt; in &lt;code&gt;kubectl describe pod&lt;/code&gt;, or &lt;code&gt;lastState.terminated&lt;/code&gt; in &lt;code&gt;kubectl get pod -o yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;That an OOM kill happened (&lt;code&gt;reason: OOMKilled&lt;/code&gt;). &lt;strong&gt;It cannot distinguish the path&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events in &lt;code&gt;kubectl describe node &amp;lt;node name&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;SystemOOM&lt;/code&gt; as a clue and verify the source in kernel logs. If there is no record, that does not make it a memcg OOM; it just means you cannot tell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no OOM-specific event on the Pod side. You only see things like &lt;code&gt;BackOff&lt;/code&gt; accompanying the restart.&lt;/p&gt;

&lt;p&gt;Check it as follows. &lt;code&gt;SystemOOM&lt;/code&gt; is an event attached to the Node, and since Node is not a namespaced object, the event itself is recorded in the &lt;code&gt;default&lt;/code&gt; namespace.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Find which node the OOMKilled Pod is on&lt;/span&gt;
kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt; &amp;lt;Pod name&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME       READY   STATUS    RESTARTS      AGE   IP            NODE
myapp-...  1/1     Running   5 (2m ago)    1h    10.0.0.1      node-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. Check whether that node's events contain SystemOOM&lt;/span&gt;
kubectl describe node node-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
Events:
  Type     Reason     Age   From     Message
  ----     ------     ----  ----     -------
  Warning  SystemOOM  3m    kubelet  System OOM encountered, victim process: java, pid: 12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# To list only events&lt;/span&gt;
kubectl get events &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SystemOOM,involvedObject.name&lt;span class="o"&gt;=&lt;/span&gt;node-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If &lt;code&gt;SystemOOM&lt;/code&gt; exists, inspect kernel messages from the same time: &lt;code&gt;constraint&lt;/code&gt;, &lt;code&gt;oom_memcg&lt;/code&gt;, &lt;code&gt;task_memcg&lt;/code&gt; and the killed PID. A parsing failure can also produce this event. Match the event and kernel log to the container's termination time (&lt;code&gt;lastState.terminated.finishedAt&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not finding &lt;code&gt;SystemOOM&lt;/code&gt; is not evidence of memcg OOM.&lt;/strong&gt; The same state results from event TTL expiry, a kubelet restart, or a missed event.&lt;/p&gt;

&lt;p&gt;The event retention period is set by kube-apiserver's &lt;code&gt;--event-ttl&lt;/code&gt;, and &lt;strong&gt;the default is 1 hour&lt;/strong&gt; (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/controlplane/apiserver/options/options.go#L129" rel="noopener noreferrer"&gt;options.go&lt;/a&gt;). So &lt;strong&gt;an old &lt;code&gt;SystemOOM&lt;/code&gt; may no longer be available through &lt;code&gt;kubectl&lt;/code&gt;&lt;/strong&gt;. If you forward events to an external logging platform, check there.&lt;/p&gt;

&lt;p&gt;On the node, inspect the kernel log:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; short-iso &lt;span class="se"&gt;\&lt;/span&gt;
  | rg &lt;span class="s1"&gt;'oom-kill:|Out of memory:|Memory cgroup out of memory:|Killed process'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;CONSTRAINT_MEMCG&lt;/code&gt; and &lt;code&gt;oom_memcg&lt;/code&gt; identify the limiting cgroup; &lt;code&gt;task_memcg&lt;/code&gt; identifies the victim's cgroup. If these differ, inspect the Pod and ancestor limits. &lt;code&gt;CONSTRAINT_CPUSET&lt;/code&gt; and &lt;code&gt;CONSTRAINT_MEMORY_POLICY&lt;/code&gt; call for checking NUMA and cpuset restrictions. Align timestamps with the container's &lt;code&gt;finishedAt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If logs are unavailable, use the following as supporting evidence rather than proof:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; was close to the limit at that time (if so, memcg OOM is likely)&lt;/li&gt;
&lt;li&gt;Whether the kubelet-view node memory utilization was high at that time (if so, global OOM is likely)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Why OOMKilled cannot distinguish the paths
&lt;/h4&gt;

&lt;p&gt;When a task exits with code 137, the container runtime (containerd) checks &lt;code&gt;oom_kill&lt;/code&gt; in the cgroup's &lt;code&gt;memory.events&lt;/code&gt;, and if it is 1 or more, it updates the termination reason to &lt;code&gt;OOMKilled&lt;/code&gt; (&lt;a href="https://github.com/containerd/containerd/blob/v2.3.5/internal/cri/server/events.go#L194-L238" rel="noopener noreferrer"&gt;events.go&lt;/a&gt;). The check is &lt;a href="https://github.com/containerd/containerd/blob/v2.3.5/internal/cri/server/events.go#L399-L427" rel="noopener noreferrer"&gt;&lt;code&gt;oomMetricsEventOccurred()&lt;/code&gt;&lt;/a&gt; in the same file.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;taskMetricsAny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cg1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMemoryOomControl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetOomKill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cg2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMemoryEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetOomKill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The definition of &lt;code&gt;oom_kill&lt;/code&gt; in &lt;code&gt;memory.events&lt;/code&gt; is "The number of processes belonging to this cgroup killed by &lt;strong&gt;any kind of&lt;/strong&gt; OOM killer". So a process chosen and killed by the kernel in a global OOM is counted too, and &lt;strong&gt;both paths produce &lt;code&gt;OOMKilled&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  How the kubelet parser produces SystemOOM
  &lt;h4&gt;
  
  
  When SystemOOM is recorded
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;SystemOOM&lt;/code&gt; is an event the kubelet records on the Node object. The kubelet parses OOM messages in the kernel log and &lt;strong&gt;records it only when the cgroup where the OOM happened is judged to be the root (&lt;code&gt;/&lt;/code&gt;)&lt;/strong&gt; (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/oom/oom_watcher_linux.go#L72-L98" rel="noopener noreferrer"&gt;oom_watcher_linux.go&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;outStream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// Count every OOM kill per container to back the&lt;/span&gt;
            &lt;span class="c"&gt;// container_oom_events_total metric.&lt;/span&gt;
            &lt;span class="n"&gt;recordOOMKill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContainerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VictimContainerName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;recordEventContainerName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="o"&gt;...&lt;/span&gt;
                &lt;span class="n"&gt;ow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Eventf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventTypeWarning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;systemOOMEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventMsg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The usual association with global OOM comes from the combination of the kernel log format and the parser. For an OOM caused by a memcg limit, the kernel prints &lt;code&gt;oom_memcg=&amp;lt;cgroup path&amp;gt;&lt;/code&gt;, whereas for a global OOM it prints &lt;code&gt;,global_oom&lt;/code&gt; instead (&lt;code&gt;mem_cgroup_print_oom_context()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L1503-L1507" rel="noopener noreferrer"&gt;mm/memcontrol.c&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memcg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pr_cont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;",oom_memcg="&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;pr_cont_cgroup_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memcg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cgroup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="nf"&gt;pr_cont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;",global_oom"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The modern-format regular expression in the kubelet's forked parser matches the format containing &lt;code&gt;oom_memcg=&lt;/code&gt; (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/third_party/forked/cadvisor/oomparser/oomparser_linux.go#L36-L43" rel="noopener noreferrer"&gt;oomparser_linux.go&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="n"&gt;containerRegexp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;regexp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`oom-kill:constraint=(.*),nodemask=(.*),cpuset=(.*),mems_allowed=(.*),oom_memcg=(.*),task_memcg=(.*),task=(.*),pid=(.*),uid=(.*)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So no cgroup can be extracted from a global OOM message, and the initial value &lt;code&gt;/&lt;/code&gt; of &lt;code&gt;OomInstance&lt;/code&gt; is left as is (&lt;code&gt;StreamOoms&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/third_party/forked/cadvisor/oomparser/oomparser_linux.go#L140-L144" rel="noopener noreferrer"&gt;oomparser_linux.go&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;            &lt;span class="n"&gt;oom&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;OomInstance&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;VictimContainerName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;TimeOfDeath&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In other words, the presence of &lt;code&gt;SystemOOM&lt;/code&gt; is not "the result of directly determining the path" but "the result of whether the kernel log could be parsed". Still, in practice it is enough to understand that &lt;strong&gt;a recorded event is a reason to inspect node-side OOM&lt;/strong&gt;, while neither presence nor absence establishes the source.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  How it appears in metrics
&lt;/h4&gt;

&lt;p&gt;Metrics do not distinguish the path with a label. The kernel's &lt;code&gt;__oom_kill_process()&lt;/code&gt; records the same event regardless of the path.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;memcg OOM&lt;/th&gt;
&lt;th&gt;global OOM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_vmstat_oom_kill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;node-exporter (&lt;code&gt;oom_kill&lt;/code&gt; in &lt;code&gt;/proc/vmstat&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Increases&lt;/td&gt;
&lt;td&gt;Increases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_oom_events_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kubelet (result of kernel log parsing)&lt;/td&gt;
&lt;td&gt;The series of the container the killed process belonged to increases&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;id="/"&lt;/code&gt; series increases, not a container series&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;oom_kill&lt;/code&gt; in cgroup v2 &lt;code&gt;memory.events&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;(a cgroup file, not a metric; containerd reads it)&lt;/td&gt;
&lt;td&gt;Increases&lt;/td&gt;
&lt;td&gt;Increases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only &lt;code&gt;container_oom_events_total&lt;/code&gt; behaves differently because, as described earlier, the parser cannot extract a cgroup in a global OOM and treats it as the root (&lt;code&gt;/&lt;/code&gt;). This is only a result of whether the log could be parsed, so do not use it to determine the path; confirm the source from &lt;code&gt;constraint&lt;/code&gt; and &lt;code&gt;oom_memcg&lt;/code&gt; in the kernel log.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Although &lt;code&gt;container_oom_events_total&lt;/code&gt; has &lt;code&gt;container_&lt;/code&gt; in its name, in v1.37 it is counted by the kubelet, not cAdvisor (&lt;code&gt;recordOOMKill()&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/oom/oom_counter.go#L40-L44" rel="noopener noreferrer"&gt;oom_counter.go&lt;/a&gt;). The meaning of the value does not change, but you have to look elsewhere when reading the code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  What to look at after narrowing down
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you judge it to be a global OOM&lt;/strong&gt;, inspect node-side memory pressure and any NUMA or cpuset constraints. Compare the memory requests and actual usage of the Pods placed on the same node, and check whether any Pod has too small a request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you judge it to be a memcg OOM&lt;/strong&gt;, review that container's memory limit and its actual usage. If the container seems to have headroom under its limit, it may have reached the limit of the Pod as a whole or of &lt;code&gt;/kubepods.slice&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you cannot decide&lt;/strong&gt;, keep both possibilities open and check both the container's limit and usage and the node's memory utilization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;/p&gt;
  Kernel OOM behavior and victim selection
  &lt;h3&gt;
  
  
  The two OOM kill paths
&lt;/h3&gt;

&lt;p&gt;There are two paths by which a container's process gets OOM killed. Both are SIGKILLs from the kernel's OOM killer, but the conditions under which they occur and the scope they affect differ. As defined in the terminology notes, memcg OOM enforces a configured limit and global OOM protects the node, so their purposes differ at the root.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;What gets killed&lt;/th&gt;
&lt;th&gt;Node event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;memcg OOM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;memory.max&lt;/code&gt; exceeded for a container, Pod or &lt;code&gt;/kubepods.slice&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Processes inside the cgroup that hit its limit&lt;/td&gt;
&lt;td&gt;Not recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;global OOM&lt;/td&gt;
&lt;td&gt;A node-side allocation cannot be satisfied after reclaim, possibly within NUMA or cpuset constraints&lt;/td&gt;
&lt;td&gt;Chosen from eligible processes under the allocation constraints&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SystemOOM&lt;/code&gt; is recorded when the kubelet parses the log and treats the triggering cgroup as &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Even if your container is under its limit, it can be OOM killed because of another Pod on the same node.&lt;/strong&gt; That is global OOM.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SystemOOM&lt;/code&gt; is a kubelet-generated clue to investigate, not a definitive classification of the OOM source. Events expire according to the API server's &lt;code&gt;--event-ttl&lt;/code&gt; (one hour by default), so a missing event does not rule out global OOM.&lt;/p&gt;

&lt;h4&gt;
  
  
  OOM kill from exceeding a cgroup limit (memcg OOM)
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;resources.limits.memory&lt;/code&gt; is set as the container cgroup's &lt;code&gt;memory.max&lt;/code&gt;. When a cgroup's &lt;code&gt;memory.current&lt;/code&gt; reaches &lt;code&gt;memory.max&lt;/code&gt;, this is what happens. As described above, limits are set not only on containers but also on Pods and &lt;code&gt;/kubepods.slice&lt;/code&gt;, and the same flow applies whichever level reaches its limit.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The kernel tries to reclaim memory inside that cgroup. Page cache is basically reclaimed here.&lt;/li&gt;
&lt;li&gt;If usage still does not fit under &lt;code&gt;memory.max&lt;/code&gt; after reclaim, the cgroup's OOM killer SIGKILLs processes in that cgroup.&lt;/li&gt;
&lt;li&gt;With cgroup v2, the kubelet defaults to &lt;code&gt;singleProcessOOMKill: false&lt;/code&gt; and sets &lt;code&gt;memory.oom.group=1&lt;/code&gt; for the container. A memcg OOM therefore normally kills the container's processes together. With &lt;code&gt;singleProcessOOMKill: true&lt;/code&gt;, a child process can be killed while the container keeps running. Container restarts follow &lt;code&gt;restartPolicy&lt;/code&gt;. See &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L393-L400" rel="noopener noreferrer"&gt;the implementation&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation is &lt;code&gt;try_charge_memcg()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L2157-L2158" rel="noopener noreferrer"&gt;mm/memcontrol.c&lt;/a&gt;. It is a goto loop returning to the &lt;code&gt;retry:&lt;/code&gt; label, and proceeds in this order.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Try to charge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L2177-L2178" rel="noopener noreferrer"&gt;L2177-L2178&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;If &lt;code&gt;page_counter_try_charge()&lt;/code&gt; succeeds, the allocation completes and it returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Reclaim&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L2211-L2216" rel="noopener noreferrer"&gt;L2211-L2216&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;On failure it reclaims with &lt;code&gt;try_to_free_mem_cgroup_pages()&lt;/code&gt;, and if &lt;code&gt;mem_cgroup_margin()&lt;/code&gt; satisfies the request, goes back to 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Count the retry&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L2244-L2245" rel="noopener noreferrer"&gt;L2244-L2245&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;If reclaim is not enough, it decrements &lt;code&gt;nr_retries&lt;/code&gt; and goes back to 1. The initial value is &lt;code&gt;MAX_RECLAIM_RETRIES&lt;/code&gt; (= &lt;code&gt;16&lt;/code&gt;; &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/internal.h#L468" rel="noopener noreferrer"&gt;mm/internal.h&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Invoke the OOM killer&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L2259-L2264" rel="noopener noreferrer"&gt;L2259-L2264&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Only after the retries are used up does it call &lt;code&gt;mem_cgroup_oom()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;MAX_RECLAIM_RETRIES&lt;/code&gt; is a retry budget, not a guarantee of exactly 16 reclaim attempts. Allocation flags and reclaim progress change the control flow. The check in step 2 looks like this in code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="n"&gt;nr_reclaimed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;try_to_free_mem_cgroup_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_over_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nr_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;gfp_mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reclaim_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;psi_memstall_leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pflags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_cgroup_margin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_over_limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;nr_pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel documentation for &lt;code&gt;memory.max&lt;/code&gt; says the same.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Memory usage hard limit. ... If a cgroup's memory usage reaches this limit &lt;strong&gt;and can't be reduced&lt;/strong&gt;, the OOM killer is invoked in the cgroup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning: &lt;code&gt;memory.current&lt;/code&gt; reaching &lt;code&gt;memory.max&lt;/code&gt; does not by itself cause an OOM.&lt;/strong&gt; A cgroup sitting at its limit with page cache is a normal state. For the same reason, &lt;strong&gt;the working set reaching &lt;code&gt;memory.max&lt;/code&gt; is not an OOM condition either&lt;/strong&gt;. Only &lt;code&gt;inactive_file&lt;/code&gt; is subtracted from the working set, and reclaimable memory remains in the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;active_file&lt;/code&gt;: demoted to inactive under pressure and then reclaimed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slab_reclaimable&lt;/code&gt; (dentry / inode caches): in memcg reclaim too, &lt;code&gt;shrink_slab()&lt;/code&gt; is called after &lt;code&gt;shrink_lruvec()&lt;/code&gt; (&lt;code&gt;shrink_node_memcgs()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5917-L5920" rel="noopener noreferrer"&gt;mm/vmscan.c&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So there is no threshold of "OOM once either value reaches the limit". The actual condition is &lt;strong&gt;when the charge fails and reclaim cannot make room&lt;/strong&gt;. What cannot be reclaimed is mainly anonymous pages (without swap) and &lt;code&gt;shmem&lt;/code&gt;, so that is where to look when narrowing down.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That said, &lt;strong&gt;what the kernel compares against &lt;code&gt;memory.max&lt;/code&gt; in a memcg OOM is the whole of &lt;code&gt;memory.current&lt;/code&gt;&lt;/strong&gt;, and it is not decided by any single metric. &lt;code&gt;memory.current&lt;/code&gt; includes, besides anonymous pages, kernel memory, &lt;code&gt;shmem&lt;/code&gt; (tmpfs and shared memory), and page cache that cannot be reclaimed, or cannot be reclaimed right away.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;container_memory_rss&lt;/code&gt; is an indicator for determining whether anonymous pages are the main cause. If cgroup RSS is close to the limit, anonymous pages are a likely major contributor, but &lt;strong&gt;RSS being below the limit is no evidence that an OOM will not occur&lt;/strong&gt;. When the gap between RSS and the working set is large, check &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; (the value of &lt;code&gt;memory.current&lt;/code&gt;) together with the breakdown in &lt;code&gt;memory.stat&lt;/code&gt; on the node: &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;shmem&lt;/code&gt;, &lt;code&gt;slab&lt;/code&gt; and so on.&lt;/p&gt;

&lt;h4&gt;
  
  
  OOM kill from node memory exhaustion (global OOM)
&lt;/h4&gt;

&lt;p&gt;When a node-side allocation cannot be satisfied after reclaim, the kernel evaluates eligible processes using &lt;code&gt;oom_badness()&lt;/code&gt;. cpusets, NUMA policies and &lt;code&gt;oom_score_adj=-1000&lt;/code&gt; can restrict the candidates. A container need not exceed its own limit to be selected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no guaranteed ordering between kubelet eviction and the kernel's OOM killer.&lt;/strong&gt; The kubelet evicts Pods to protect the node, and its selection puts "Pods whose usage exceeds their memory request" first (&lt;code&gt;rankMemoryPressure()&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/eviction/helpers.go#L816-L821" rel="noopener noreferrer"&gt;helpers.go&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// rankMemoryPressure orders the input pods for eviction in response to memory pressure.&lt;/span&gt;
&lt;span class="c"&gt;// It ranks by whether or not the pod's usage exceeds its requests, then by priority, and&lt;/span&gt;
&lt;span class="c"&gt;// finally by memory usage above requests.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;rankMemoryPressure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pods&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="n"&gt;statsFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orderedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exceedMemoryRequests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Pod with a zero memory request and positive usage belongs to the "exceeds request" group. Within that group, priority and then usage above request determine the order, so it is not necessarily evicted first. If eviction acts in time, the result is &lt;strong&gt;&lt;code&gt;Evicted&lt;/code&gt;, not &lt;code&gt;OOMKilled&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Global OOM happens first &lt;strong&gt;when eviction cannot reclaim memory as fast as memory grows&lt;/strong&gt;. There are several reasons it cannot keep up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation interval&lt;/td&gt;
&lt;td&gt;10 seconds (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/kubelet.go#L197" rel="noopener noreferrer"&gt;&lt;code&gt;evictionMonitoringPeriod&lt;/code&gt;&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Detection can be up to 10 seconds late&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pods evicted per evaluation&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Only one&lt;/strong&gt; ("we kill at most a single pod during each eviction interval" in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/eviction/eviction_manager.go#L428-L451" rel="noopener noreferrer"&gt;eviction_manager.go&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;At most one Pod is evicted per synchronization; notifications and cleanup waits mean this is not a fixed rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waiting for cleanup&lt;/td&gt;
&lt;td&gt;Up to 30 seconds (&lt;code&gt;podCleanupTimeout&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The next evaluation may be delayed further&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default threshold&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.available&amp;lt;100Mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extremely thin on large nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How &lt;code&gt;memory.available&lt;/code&gt; is computed&lt;/td&gt;
&lt;td&gt;From the root cgroup's &lt;code&gt;anon + file&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Contains no kernel memory, so it overestimates free memory
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official Kubernetes documentation also says (&lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior" rel="noopener noreferrer"&gt;Node out of memory behavior&lt;/a&gt;):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the node experiences an out of memory (OOM) event prior to the kubelet being able to reclaim memory, the node depends on the oom_killer to respond.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is more likely when applications that allocate a lot of memory right at startup, or in response to a sudden surge in requests, share a node.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning: Eviction protects the node; it does not protect individual Pods from OOM.&lt;/strong&gt; Containers without &lt;code&gt;limits&lt;/code&gt; have no &lt;code&gt;memory.max&lt;/code&gt; backstop at all, so eviction is merely the last line of defense.&lt;/p&gt;

&lt;p&gt;Moreover, &lt;code&gt;rankMemoryPressure()&lt;/code&gt; looks at &lt;strong&gt;Pod priority before usage size&lt;/strong&gt;. If the offending Pod has high priority, low-priority Pods with small usage get evicted first, and global OOM can occur without the pressure being resolved. The proper countermeasure is to set &lt;code&gt;limits&lt;/code&gt; and &lt;code&gt;requests&lt;/code&gt; rather than relying on eviction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h5&gt;
  
  
  Which process gets killed
&lt;/h5&gt;

&lt;p&gt;The kernel evaluates eligible processes using resident pages, swap, page tables and &lt;code&gt;oom_score_adj&lt;/code&gt;; the adjustment alone does not establish a fixed kill order. The kubelet sets &lt;code&gt;oom_score_adj&lt;/code&gt; (a bonus added to &lt;code&gt;oom_score&lt;/code&gt;) according to the container's QoS class, so &lt;strong&gt;the QoS class and the memory request setting directly become how likely a container is to be killed&lt;/strong&gt;. You can check the QoS class with &lt;code&gt;kubectl get pod &amp;lt;Pod name&amp;gt; -o jsonpath='{.status.qosClass}'&lt;/code&gt;, and how it is determined is described in &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/" rel="noopener noreferrer"&gt;Pod Quality of Service Classes&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;&lt;code&gt;oom_score_adj&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Likelihood of being killed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;system-node-critical&lt;/code&gt;&lt;/strong&gt; (evaluated before the QoS class)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-997&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Least likely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guaranteed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-997&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Least likely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burstable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1000 - 1000 × memory request / node memory capacity&lt;/code&gt; (clamped to 3–999)&lt;/td&gt;
&lt;td&gt;The smaller the request, the more likely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BestEffort&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Most likely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The implementation is &lt;code&gt;GetContainerOOMScoreAdjust()&lt;/code&gt; in Kubernetes' &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/qos/policy.go#L47-L59" rel="noopener noreferrer"&gt;policy.go&lt;/a&gt;. &lt;code&gt;system-node-critical&lt;/code&gt; is evaluated before the QoS class decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsNodeCriticalPod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Only node critical pod should be the last to get killed.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;guaranteedOOMScoreAdj&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The formula above is the container-level baseline. Pod-level requests and restartable init containers can introduce adjustments. An omitted memory request can also default from the limit, so inspect the applied Pod spec.&lt;/p&gt;

&lt;p&gt;The Burstable clamp is implemented with a lower bound of &lt;code&gt;1000 + guaranteedOOMScoreAdj&lt;/code&gt; (= 3) and an upper bound of &lt;code&gt;besteffortOOMScoreAdj - 1&lt;/code&gt; (= 999). A Pod whose request is set smaller than its actual usage is more likely to be targeted in both eviction and global OOM.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning: PriorityClass works differently for eviction and for global OOM.&lt;/strong&gt; It is a misreading to think that raising priority protects you from both.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PriorityClass&lt;/th&gt;
&lt;th&gt;Eviction&lt;/th&gt;
&lt;th&gt;global OOM (&lt;code&gt;oom_score_adj&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User defined (up to &lt;code&gt;1000000000&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The higher the value, the later it is evicted&lt;/td&gt;
&lt;td&gt;Determined by QoS class and request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system-cluster-critical&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Exempt from node-pressure eviction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Determined by QoS class and request (&lt;strong&gt;not protected&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system-node-critical&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Exempt from node-pressure eviction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fixed at &lt;code&gt;-997&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static / mirror Pod&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Exempt from node-pressure eviction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Determined by QoS class and request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three exempt rows identify critical Pods for which &lt;code&gt;IsCriticalPod()&lt;/code&gt; is true; the kubelet checks static and mirror Pods as well as system-critical priority classes.&lt;/p&gt;

&lt;p&gt;What &lt;code&gt;oom_score_adj&lt;/code&gt; protects is only &lt;code&gt;system-node-critical&lt;/code&gt;, for which &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/types/pod_update.go#L190-L193" rel="noopener noreferrer"&gt;&lt;code&gt;IsNodeCriticalPod()&lt;/code&gt;&lt;/a&gt; is true. &lt;strong&gt;If a &lt;code&gt;system-cluster-critical&lt;/code&gt; Pod is BestEffort, its &lt;code&gt;oom_score_adj&lt;/code&gt; is &lt;code&gt;1000&lt;/code&gt;&lt;/strong&gt;, and it is more likely to be selected in a global OOM. "I raised the priority but it was still &lt;code&gt;OOMKilled&lt;/code&gt;" comes from this path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Neither an OOM kill nor node-pressure eviction directly deletes the Pod API object. What happens next depends on &lt;code&gt;restartPolicy&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Always&lt;/code&gt; or &lt;code&gt;OnFailure&lt;/code&gt;: the container is recreated on the same node. &lt;strong&gt;The Pod stays on the same node&lt;/strong&gt;, so if the cause is not resolved, OOM kills will happen again&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Never&lt;/code&gt;: the container is not recreated and the Pod remains &lt;code&gt;Failed&lt;/code&gt;. If a controller such as a Job creates a successor Pod, it may be placed on a different node&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;OOMKilled&lt;/code&gt; repeats even though there is headroom under the limit, suspect this path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Because the OOM kill frees memory, the pressure may already be gone when the kubelet next evaluates free memory. In that case the node may get no &lt;code&gt;MemoryPressure&lt;/code&gt; condition and no &lt;code&gt;node.kubernetes.io/memory-pressure:NoSchedule&lt;/code&gt; taint, and scheduling of new Pods does not stop. Conversely, if the pressure continues, a later evaluation sets the condition and taint, and eviction also occurs. Note that it is decided by &lt;code&gt;memory.available&lt;/code&gt; at evaluation time, not by whether an OOM kill happened.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Checking with PSI (Pressure Stall Information)
&lt;/h4&gt;

&lt;p&gt;This is an auxiliary indicator for the case where things are slow although usage has not reached the limit. It shows how long processes were actually stalled waiting for memory reclaim.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  How to read PSI and PromQL
  &lt;p&gt;You can obtain the time a container was stalled waiting for memory allocation as PSI. Even when usage has not reached the limit, you can detect being made to wait by memory reclaim.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_pressure_memory_waiting_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;Time some processes were waiting for memory (&lt;code&gt;total&lt;/code&gt; of &lt;code&gt;some&lt;/code&gt; in &lt;code&gt;memory.pressure&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_pressure_memory_stalled_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;Time all processes were stalled waiting for memory (&lt;code&gt;total&lt;/code&gt; of &lt;code&gt;full&lt;/code&gt; in &lt;code&gt;memory.pressure&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; These metrics are controlled by the &lt;code&gt;KubeletPSI&lt;/code&gt; feature gate, which &lt;strong&gt;went GA in v1.36 and is enabled by default (it cannot be disabled)&lt;/strong&gt;. The kernel must support PSI, though, and the kubelet decides this by whether &lt;code&gt;cpu.pressure&lt;/code&gt; exists at the cgroup root (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/cadvisor/cadvisor_linux.go#L172-L187" rel="noopener noreferrer"&gt;cadvisor_linux.go&lt;/a&gt;). If the metrics do not appear, check whether the kernel was built with &lt;code&gt;CONFIG_PSI=y&lt;/code&gt; and whether &lt;code&gt;psi=0&lt;/code&gt; is set as a kernel parameter.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;memory.pressure&lt;/code&gt; also has &lt;code&gt;avg10&lt;/code&gt; / &lt;code&gt;avg60&lt;/code&gt; / &lt;code&gt;avg300&lt;/code&gt;, "the average over the last N seconds (%)", but &lt;strong&gt;only the cumulative value (&lt;code&gt;total&lt;/code&gt;) is exposed as a Prometheus metric&lt;/strong&gt;. So use &lt;code&gt;rate()&lt;/code&gt; to see a ratio.&lt;/p&gt;

&lt;p&gt;The unit is seconds, so the result of &lt;code&gt;rate()&lt;/code&gt; is "how many seconds of waiting per second", a ratio from 0 to 1. Multiply by 100 to get a percentage directly.&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Per Pod, the share of time (%) all processes were stalled waiting for memory
&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container_pressure_memory_stalled_seconds_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Per Pod, the share of time (%) some processes were waiting for memory
&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container_pressure_memory_waiting_seconds_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When there is plenty of memory, neither increases, so &lt;code&gt;rate()&lt;/code&gt; stays at 0. Under pressure, &lt;strong&gt;&lt;code&gt;waiting&lt;/code&gt; (some) rises first, and if it gets worse &lt;code&gt;stalled&lt;/code&gt; (full) follows&lt;/strong&gt;. &lt;code&gt;waiting&lt;/code&gt; counts even when only some processes waited, so &lt;strong&gt;to judge severity, use &lt;code&gt;stalled&lt;/code&gt;&lt;/strong&gt;. It is the time all processes in the container were stopped, so if it shows a value, throughput is directly affected.&lt;/p&gt;

&lt;p&gt;To see memory pressure on the whole node, use the root cgroup series.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Share of time (%) the whole node was completely stalled waiting for memory
&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container_pressure_memory_stalled_seconds_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;group_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;kubelet_node_name&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers that are struggling although they are below their limit can be found by combining PSI with utilization. &lt;code&gt;container_spec_memory_limit_bytes&lt;/code&gt; is the container limit exposed by cAdvisor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Containers whose usage is under 80 % of the limit but that are stalled waiting for memory
&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;container_pressure_memory_stalled_seconds_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="ow"&gt;and&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_usage_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;container_spec_memory_limit_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;and&lt;/code&gt; keeps only series whose label sets match exactly on both sides. A container with no limit has &lt;code&gt;container_spec_memory_limit_bytes&lt;/code&gt; of 0, so the division gives &lt;code&gt;+Inf&lt;/code&gt;, which does not satisfy &lt;code&gt;&amp;lt; 0.8&lt;/code&gt;, and it is excluded automatically.&lt;/p&gt;

&lt;p&gt;Here is an example alert. It catches &lt;code&gt;stalled&lt;/code&gt; that keeps occurring.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ContainerMemoryStalled&lt;/span&gt;
  &lt;span class="c1"&gt;# Fires when the 5-minute average of fully stalled time is 5 % or more for 10 minutes&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;100 * max by (namespace, pod) (&lt;/span&gt;
      &lt;span class="s"&gt;rate(container_pressure_memory_stalled_seconds_total{container="", image="", pod!=""}[5m])&lt;/span&gt;
    &lt;span class="s"&gt;) &amp;gt; 5&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stalled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;waiting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;allocation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(add&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;namespace&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;labels&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;own&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;template)"&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;may&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;made&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wait&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reclaim&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;even&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;though&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reached&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;its&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;limit.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;between&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;RSS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cache."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right threshold of &lt;code&gt;5 %&lt;/code&gt; depends on the environment. First look at the distribution of values without an alert to understand the normal level, then decide.&lt;/p&gt;

&lt;p&gt;For details on PSI see &lt;a href="https://kubernetes.io/docs/reference/instrumentation/understand-psi-metrics/" rel="noopener noreferrer"&gt;Understand Pressure Stall Information (PSI) Metrics&lt;/a&gt;.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference: contradictions explained and a metric map
&lt;/h2&gt;

&lt;p&gt;The working set, cgroup usage, process RSS and node availability answer different questions. Use this map to identify the value behind the dashboard before comparing it with a limit or threshold. For a non-root cgroup, working set is &lt;code&gt;max(memory.current - inactive_file, 0)&lt;/code&gt;. It still includes some reclaimable memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Backing value in cgroup v2 / &lt;code&gt;/proc&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Page cache handling&lt;/th&gt;
&lt;th&gt;Main use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max(memory.current − inactive_file, 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active pages count as in use&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl top pod&lt;/code&gt;, usage visualization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_memory_rss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anon&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not included (anonymous pages only)&lt;/td&gt;
&lt;td&gt;Understanding what the app allocated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_memory_usage_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.current&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everything included&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;What memcg OOM is evaluated against&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_memory_working_set_bytes{id="/"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anon + file − inactive_file&lt;/code&gt; in the root cgroup's &lt;code&gt;memory.stat&lt;/code&gt; (see below)&lt;/td&gt;
&lt;td&gt;Active pages count as in use&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Eviction decisions&lt;/strong&gt;, &lt;code&gt;kubectl top node&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;node-exporter&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MemAvailable&lt;/code&gt; in &lt;code&gt;/proc/meminfo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reclaimable pages count as free&lt;/td&gt;
&lt;td&gt;Visualizing node memory utilization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kube_node_status_capacity{resource="memory"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kube-state-metrics&lt;/td&gt;
&lt;td&gt;(&lt;code&gt;status.capacity&lt;/code&gt; of the Node object)&lt;/td&gt;
&lt;td&gt;(capacity, not usage)&lt;/td&gt;
&lt;td&gt;Denominator when converting to utilization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Looking at the same node, utilization computed from &lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt; does not match the utilization the kubelet uses for eviction decisions. With workloads that do a lot of file I/O or logging, the difference can reach several GiB.&lt;/p&gt;

&lt;p&gt;Different emitters also mean &lt;strong&gt;different labels on the metrics&lt;/strong&gt;. cAdvisor metrics carry no label identifying the node, so when you compute per-node utilization you need to align labels with &lt;code&gt;kubelet_node_name&lt;/code&gt;, as shown later.&lt;/p&gt;
&lt;h2&gt;
  
  
  Kubernetes memory usage and cgroup v2
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Why the numbers differ
&lt;/h3&gt;

&lt;p&gt;A process RSS measurement includes its resident mappings, including shared and file-backed pages. Cgroup accounting charges memory to a group, while working set subtracts one category of file cache from usage. An application's heap metric covers a different subset again.&lt;/p&gt;

&lt;p&gt;Reclaimability is a separate question: clean file cache can often be reclaimed, dirty pages may need writeback, and anonymous memory normally needs swap or explicit release by the process. Calling &lt;code&gt;free&lt;/code&gt; does not necessarily return allocator-held memory to the OS.&lt;/p&gt;

&lt;p&gt;These differences explain why heap size, process RSS, cgroup RSS and working set need not match. The following sections connect those values to cgroup v2 accounting.&lt;/p&gt;
&lt;h3&gt;
  
  
  Memory breakdown in cgroup v2
&lt;/h3&gt;

&lt;p&gt;Container memory usage is managed in a per-container cgroup. In cgroup v2, &lt;code&gt;memory.current&lt;/code&gt; is the current usage and &lt;code&gt;memory.stat&lt;/code&gt; is its breakdown.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example values in a container's cgroup (checked on the node)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/&amp;lt;container cgroup&amp;gt;/memory.current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5368709120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/&amp;lt;container cgroup&amp;gt;/memory.stat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anon 2147483648           # anonymous pages (heap, stack: memory not backed by a file)
file 3087007744           # page cache (including tmpfs and shared memory)
shmem 268435456           # the part of file that is tmpfs or shared memory
...
inactive_anon 134217728    # anon LRU: not accessed for a while
active_anon 2013265920     # anon LRU: recently accessed
inactive_file 1073741824   # file LRU: not accessed for a while
active_file 1744830464     # file LRU: recently accessed
unevictable 268435456      # pages that cannot be reclaimed (here, the tmpfs part)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The following relationships hold in this example. &lt;strong&gt;Note that &lt;code&gt;inactive_file + active_file&lt;/code&gt; does not equal &lt;code&gt;file&lt;/code&gt;; it equals &lt;code&gt;file - shmem&lt;/code&gt;.&lt;/strong&gt; The next section explains why.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anon + file + kernel memory = 5368709120 (memory.current)
inactive_file + active_file = 2818572288 = file - shmem
inactive_anon + active_anon = 2147483648 = anon        (shmem is not in here)
unevictable                 =  268435456 = shmem       (tmpfs without swap)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The main fields of &lt;code&gt;memory.stat&lt;/code&gt; are:&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;anon&lt;/code&gt; (anonymous pages)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Memory not backed by a file. Regions the application allocated with &lt;code&gt;malloc&lt;/code&gt; and similar belong here.&lt;/li&gt;
&lt;li&gt;Unlike page cache, &lt;strong&gt;the kernel cannot reclaim it independently of the process&lt;/strong&gt;. With swap it can write pages out and free up space, but on nodes that do not use swap it is &lt;strong&gt;memory the kernel cannot reclaim&lt;/strong&gt; under pressure. When swap is unavailable, the kernel sets &lt;code&gt;scan_balance = SCAN_FILE&lt;/code&gt; and excludes the anon LRU from scanning (&lt;code&gt;get_scan_count()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L2382-L2386" rel="noopener noreferrer"&gt;mm/vmscan.c&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;On the other hand, this value drops if the process itself returns memory to the OS with &lt;code&gt;munmap&lt;/code&gt; or &lt;code&gt;madvise(MADV_DONTNEED)&lt;/code&gt;. Note that calling &lt;code&gt;free&lt;/code&gt; does not reduce it unless the allocator or language runtime returns the memory to the OS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;file&lt;/code&gt; (page cache)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Contents of files held in memory by the kernel during reads and writes. The application did not allocate it explicitly.&lt;/li&gt;
&lt;li&gt;When memory runs short the kernel discards (reclaims) it automatically, so it is &lt;strong&gt;basically reclaimable memory&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Cache of files on disk is split into &lt;code&gt;active_file&lt;/code&gt; (recently accessed) and &lt;code&gt;inactive_file&lt;/code&gt; (not accessed for a while). Reclaim takes &lt;code&gt;inactive_file&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;active_file + inactive_file&lt;/code&gt; does not equal &lt;code&gt;file&lt;/code&gt;.&lt;/strong&gt; These are totals of the LRU lists, and as described below &lt;code&gt;shmem&lt;/code&gt; is on the anon LRU rather than the file LRU. Besides &lt;code&gt;shmem&lt;/code&gt;, pages that fall off the LRU, such as &lt;code&gt;mlock&lt;/code&gt;ed pages, also show up as a difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It includes not only the cache of files on disk but also &lt;code&gt;shmem&lt;/code&gt; (tmpfs and shared memory)&lt;/strong&gt;, because Linux treats these as page cache too. &lt;code&gt;shmem&lt;/code&gt; is written out to swap rather than to a file, so on nodes without swap it cannot be reclaimed. The breakdown is visible as &lt;code&gt;shmem&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning: &lt;code&gt;shmem&lt;/code&gt; is counted in &lt;code&gt;file&lt;/code&gt;, but it is not on the file LRU.&lt;/strong&gt; The kernel classifies swap-backed pages as anon, and tmpfs pages are among them. &lt;code&gt;folio_is_file_lru()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/include/linux/mm_inline.h#L13-L31" rel="noopener noreferrer"&gt;mm_inline.h&lt;/a&gt; is defined as "0 if &lt;code&gt;@folio&lt;/code&gt; is a normal anonymous folio, &lt;strong&gt;a tmpfs folio&lt;/strong&gt; or otherwise ram or swap backed folio".&lt;/p&gt;

&lt;p&gt;The counters they are charged to are different, though. A tmpfs page is added to both &lt;code&gt;NR_FILE_PAGES&lt;/code&gt; (= &lt;code&gt;file&lt;/code&gt;) and &lt;code&gt;NR_SHMEM&lt;/code&gt; (= &lt;code&gt;shmem&lt;/code&gt;) (&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/shmem.c#L818-L819" rel="noopener noreferrer"&gt;mm/shmem.c&lt;/a&gt;):&lt;/p&gt;


&lt;pre class="highlight c"&gt;&lt;code&gt;  &lt;span class="n"&gt;__lruvec_stat_mod_folio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NR_FILE_PAGES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;__lruvec_stat_mod_folio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NR_SHMEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Furthermore, for a Kubernetes &lt;code&gt;emptyDir&lt;/code&gt; with &lt;code&gt;medium: Memory&lt;/code&gt;, &lt;strong&gt;the kubelet mounts tmpfs with the &lt;code&gt;noswap&lt;/code&gt; option when the node supports it&lt;/strong&gt; (&lt;code&gt;generateTmpfsMountOptions()&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/volume/emptydir/empty_dir.go#L601-L612" rel="noopener noreferrer"&gt;empty_dir.go&lt;/a&gt;). The kernel makes the inode of a &lt;code&gt;noswap&lt;/code&gt; tmpfs unevictable (&lt;code&gt;shmem_get_inode()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/shmem.c#L2824-L2825" rel="noopener noreferrer"&gt;mm/shmem.c&lt;/a&gt;), so these pages sit on the &lt;strong&gt;unevictable LRU&lt;/strong&gt;, not the anon LRU.&lt;/p&gt;


&lt;pre class="highlight c"&gt;&lt;code&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sbinfo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;noswap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;mapping_set_unevictable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;i_mapping&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;So &lt;code&gt;shmem&lt;/code&gt; is accounted as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mount&lt;/th&gt;
&lt;th&gt;LRU&lt;/th&gt;
&lt;th&gt;Where it shows up in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;emptyDir&lt;/code&gt; with &lt;code&gt;medium: Memory&lt;/code&gt; (&lt;code&gt;noswap&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;unevictable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unevictable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any other tmpfs / shared memory&lt;/td&gt;
&lt;td&gt;anon LRU&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inactive_anon&lt;/code&gt; / &lt;code&gt;active_anon&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;In neither case does it enter &lt;code&gt;inactive_file&lt;/code&gt; / &lt;code&gt;active_file&lt;/code&gt;.&lt;/strong&gt; That matters in two ways later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inactive_file + active_file&lt;/code&gt; generally corresponds to &lt;code&gt;file - shmem&lt;/code&gt;; unevictable pages and asynchronous statistics can cause differences&lt;/li&gt;
&lt;li&gt;Only &lt;code&gt;inactive_file&lt;/code&gt; is subtracted from the working set, so &lt;strong&gt;&lt;code&gt;shmem&lt;/code&gt; stays in the working set in full&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;With workloads that write a lot of logs or read and write large files, the page cache can grow to several GiB even though the application did not intend that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;memory.current&lt;/code&gt; includes memory the kernel itself uses (slab, socket buffers and so on) in addition to &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;file&lt;/code&gt;. So adding &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;file&lt;/code&gt; alone does not give &lt;code&gt;memory.current&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  How cgroup v2 memory controls map to Pod settings
&lt;/h3&gt;
&lt;h4&gt;
  
  
  cgroup v2 memory control interface
&lt;/h4&gt;

&lt;p&gt;cgroup v2 offers four levels of control over memory usage. &lt;code&gt;memory.min&lt;/code&gt; and &lt;code&gt;memory.low&lt;/code&gt; are &lt;strong&gt;protections&lt;/strong&gt; ("do not reclaim up to here"), and &lt;code&gt;memory.high&lt;/code&gt; and &lt;code&gt;memory.max&lt;/code&gt; are &lt;strong&gt;limits&lt;/strong&gt; ("do not allow use beyond here").&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.min&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protection (hard)&lt;/td&gt;
&lt;td&gt;Protects memory within the effective boundary determined by ancestors and usage. It does not preallocate physical memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protection (soft)&lt;/td&gt;
&lt;td&gt;Best-effort avoidance of reclaim up to this value. It is reclaimed if nothing else can be&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limit (soft)&lt;/td&gt;
&lt;td&gt;Exceeding it applies reclaim pressure and throttling. Crossing &lt;code&gt;memory.high&lt;/code&gt; does not itself invoke the OOM killer; &lt;code&gt;memory.max&lt;/code&gt; and global OOM still apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limit (hard)&lt;/td&gt;
&lt;td&gt;Exceeding it triggers reclaim, and if usage still does not fit, the cgroup's OOM killer runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other files that report state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memory.current&lt;/code&gt;: current usage (what &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; is backed by)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.stat&lt;/code&gt;: usage breakdown&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.events&lt;/code&gt;: counts of &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;oom&lt;/code&gt;, &lt;code&gt;oom_kill&lt;/code&gt; and &lt;code&gt;oom_group_kill&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.swap.max&lt;/code&gt;: swap limit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.pressure&lt;/code&gt;: PSI for waiting on memory allocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For exact definitions of each file see the kernel's &lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files" rel="noopener noreferrer"&gt;Memory Interface Files&lt;/a&gt;, and for how Kubernetes treats cgroup v2 see &lt;a href="https://kubernetes.io/docs/concepts/architecture/cgroups/" rel="noopener noreferrer"&gt;About cgroup v2&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  How Pod settings are reflected
&lt;/h4&gt;

&lt;p&gt;Pod settings are reflected into cgroup v2 as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pod setting&lt;/th&gt;
&lt;th&gt;Target in cgroup v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resources.limits.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.max&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resources.requests.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not reflected&lt;/strong&gt; (with the default kubelet configuration in v1.37; you can reflect it by changing settings → KEP-2570)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resources.limits.cpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cpu.max&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resources.requests.cpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cpu.weight&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;cgroup v2 is a directory tree. Its top is the &lt;strong&gt;root cgroup&lt;/strong&gt; (&lt;code&gt;/sys/fs/cgroup&lt;/code&gt;), where any process that is in no child cgroup belongs, so &lt;strong&gt;it contains not only Pods but every process on the node, such as the kubelet and containerd&lt;/strong&gt;. Kubernetes creates the hierarchy below it: "all Pods on the node → Pod → container".&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# With the systemd cgroup driver (the kubeadm default; names differ with the cgroupfs driver, see below)
/sys/fs/cgroup                                  ← root cgroup (id="/")
│                                                 contains every process on the node
│                                                 the value used for eviction decisions
│
├── system.slice/                               kubelet, containerd, sshd, etc.
│   ├── kubelet.service                           (processes that are not Pods)
│   └── containerd.service
│
└── kubepods.slice/                             ← all Pods on the node
    │                                             (id="/kubepods.slice")
    ├── kubepods-pod&amp;lt;UID&amp;gt;.slice/                ← Pod (Guaranteed)
    │   ├── cri-containerd-&amp;lt;ID&amp;gt;.scope           ← container
    │   └── cri-containerd-&amp;lt;pause ID&amp;gt;.scope     ← pause container
    │
    ├── kubepods-burstable.slice/                 (intermediate slice per QoS class)
    │   └── kubepods-burstable-pod&amp;lt;UID&amp;gt;.slice/  ← Pod (Burstable)
    │       └── cri-containerd-&amp;lt;ID&amp;gt;.scope       ← container
    │
    └── kubepods-besteffort.slice/
        └── kubepods-besteffort-pod&amp;lt;UID&amp;gt;.slice/ ← Pod (BestEffort)
            └── cri-containerd-&amp;lt;ID&amp;gt;.scope       ← container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When filtering metrics, the root cgroup is &lt;code&gt;id="/"&lt;/code&gt;, all Pods on the node are &lt;code&gt;id="/kubepods.slice"&lt;/code&gt;, a Pod is &lt;code&gt;container="", image="", pod!=""&lt;/code&gt;, and a container is &lt;code&gt;container!="", image!=""&lt;/code&gt;. &lt;strong&gt;What the kubelet uses for eviction decisions is the working set of the topmost root cgroup&lt;/strong&gt; (see below). That is a different level from memcg OOM caused by a container or Pod exceeding its limit.&lt;/p&gt;

&lt;p&gt;Memory limits are set not only on containers but also on higher levels.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;cgroup&lt;/th&gt;
&lt;th&gt;Memory limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whole node (root cgroup)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not set&lt;/strong&gt; (the root has no &lt;code&gt;memory.max&lt;/code&gt; → see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All Pods on the node&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/kubepods.slice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Capacity - kube-reserved - system-reserved&lt;/code&gt; when Pod allocatable enforcement is enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod&lt;/td&gt;
&lt;td&gt;Differs by QoS class (see below)&lt;/td&gt;
&lt;td&gt;The Pod's effective limit, including Pod-level &lt;code&gt;spec.resources.limits.memory&lt;/code&gt; when configured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cri-containerd-&amp;lt;ID&amp;gt;.scope&lt;/code&gt; under the Pod's cgroup&lt;/td&gt;
&lt;td&gt;That container's &lt;code&gt;limits.memory&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The examples here use container-level resources and omit Pod-level resources. Without a Pod-level limit, the required container limits must be present for a finite Pod limit to be set. A Pod's effective limit is not simply the sum of its regular containers' &lt;code&gt;limits.memory&lt;/code&gt;. If there are init containers, their maximum is also considered, and &lt;code&gt;spec.overhead&lt;/code&gt; is added if set. For the calculation see &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#resource-sharing-within-containers" rel="noopener noreferrer"&gt;Resource sharing within containers&lt;/a&gt; and &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/pod-overhead/" rel="noopener noreferrer"&gt;Pod Overhead&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;even if a container has not reached its own limit, an OOM kill can occur when the Pod as a whole or &lt;code&gt;/kubepods.slice&lt;/code&gt; hits its limit&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The paths above assume the kubelet uses the systemd cgroup driver. With the cgroupfs driver, paths look like &lt;code&gt;/kubepods/burstable/pod&amp;lt;UID&amp;gt;/&amp;lt;container ID&amp;gt;&lt;/code&gt;, with no &lt;code&gt;.slice&lt;/code&gt; or &lt;code&gt;.scope&lt;/code&gt; and no &lt;code&gt;cri-containerd-&lt;/code&gt; prefix on the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kubelet's own default is &lt;code&gt;cgroupfs&lt;/code&gt;, but kubeadm sets &lt;code&gt;systemd&lt;/code&gt; when the value is empty&lt;/strong&gt; (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/apis/config/v1beta1/defaults.go#L168-L170" rel="noopener noreferrer"&gt;defaults.go&lt;/a&gt;, &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/cmd/kubeadm/app/componentconfigs/kubelet.go#L194-L196" rel="noopener noreferrer"&gt;kubelet.go&lt;/a&gt;). In v1.37, a supported runtime's CRI &lt;code&gt;RuntimeConfig&lt;/code&gt; response takes precedence through driver auto-detection. Check the effective driver with:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="s2"&gt;"/api/v1/nodes/&amp;lt;node name&amp;gt;/proxy/configz"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.kubeletconfig.cgroupDriver'&lt;/span&gt;
&lt;span class="s2"&gt;"systemd"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; label of cAdvisor metrics also changes with the cgroup driver, so when filtering in PromQL, check the value in your own cluster with this query:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;group by &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;container_memory_working_set_bytes&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~&lt;span class="s2"&gt;"/kubepods|/kubepods.slice"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;A Pod's cgroup path depends on its QoS class. As the tree above shows, Burstable and BestEffort go through an intermediate slice per QoS class.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;&amp;lt;UID&amp;gt;&lt;/code&gt; part, systemd escaping replaces the hyphens of the Pod UID with underscores (example: &lt;code&gt;6983351e-4ede-...&lt;/code&gt; → &lt;code&gt;pod6983351e_4ede_...&lt;/code&gt;). The UID of a static Pod (mirror Pod) is a hash with no hyphens, so it looks like &lt;code&gt;pod1ff70b81885169da4e9e32c9530b77c9.slice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The QoS class and UID of a Pod can be read from the Pod resource, and once you have those two you can assemble the cgroup path.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt; &amp;lt;Pod name&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.qosClass}{"\n"}{.metadata.uid}{"\n"}'&lt;/span&gt;
Burstable
6983351e-4ede-4611-8bab-b986614af18d

&lt;span class="c"&gt;# To list them&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'NAME:.metadata.name,QOS:.status.qosClass'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Container metrics
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/google/cadvisor" rel="noopener noreferrer"&gt;cAdvisor&lt;/a&gt; built into the kubelet exposes the cgroup values above as Prometheus metrics. They are served from the kubelet's &lt;code&gt;/metrics/cadvisor&lt;/code&gt; endpoint, and you do not need to run a separate cAdvisor Pod or DaemonSet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Backing value in cgroup v2&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_usage_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.current&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usage including anonymous pages, page cache and kernel memory. The value memcg OOM is evaluated against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.current - inactive_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usage minus &lt;code&gt;inactive_file&lt;/code&gt;, clamped to zero. A heuristic that still includes some reclaimable memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_rss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anon&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Amount of anonymous pages. Unlike true RSS, it does not include &lt;code&gt;mmap&lt;/code&gt;ed file pages (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Page cache size. Includes &lt;code&gt;shmem&lt;/code&gt; (tmpfs and shared memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_total_active_file_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;active_file&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Active page cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_total_inactive_file_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inactive_file&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inactive page cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_mapped_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file_mapped&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Amount of &lt;code&gt;mmap&lt;/code&gt;ed files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_pressure_memory_waiting_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;total&lt;/code&gt; of &lt;code&gt;some&lt;/code&gt; in &lt;code&gt;memory.pressure&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Time some processes were waiting for memory (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_pressure_memory_stalled_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;total&lt;/code&gt; of &lt;code&gt;full&lt;/code&gt; in &lt;code&gt;memory.pressure&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Time all processes were stalled waiting for memory (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_oom_events_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;kubelet&lt;/strong&gt; (v1.37; the name starts with &lt;code&gt;container_&lt;/code&gt; but it is not from cAdvisor)&lt;/td&gt;
&lt;td&gt;(not a cgroup value)&lt;/td&gt;
&lt;td&gt;Counted by parsing &lt;code&gt;oom-kill:&lt;/code&gt; lines in the kernel log. &lt;strong&gt;The live series disappears when the container cgroup is removed&lt;/strong&gt; (whether or not it is recreated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The kubelet has endpoints such as &lt;code&gt;/metrics&lt;/code&gt;, &lt;code&gt;/metrics/cadvisor&lt;/code&gt; and &lt;code&gt;/metrics/resource&lt;/code&gt; (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/server/server.go#L110-L113" rel="noopener noreferrer"&gt;server.go&lt;/a&gt;). The last two relate to container and node usage. &lt;code&gt;/metrics/cadvisor&lt;/code&gt; exposes the &lt;code&gt;container_*&lt;/code&gt; metrics above, while &lt;code&gt;/metrics/resource&lt;/code&gt; exposes &lt;strong&gt;only CPU, memory and swap usage&lt;/strong&gt; (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/metrics/collectors/resource_metrics.go#L29-L120" rel="noopener noreferrer"&gt;resource_metrics.go&lt;/a&gt;). The latter has &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; and also &lt;strong&gt;per-Pod and per-node series (&lt;code&gt;pod_memory_working_set_bytes&lt;/code&gt;, &lt;code&gt;node_memory_working_set_bytes&lt;/code&gt;)&lt;/strong&gt; (see &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/metrics/collectors/resource_metrics.go#L86-L91" rel="noopener noreferrer"&gt;&lt;code&gt;podMemoryUsageDesc&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/metrics/collectors/resource_metrics.go#L37-L42" rel="noopener noreferrer"&gt;&lt;code&gt;nodeMemoryUsageDesc&lt;/code&gt;&lt;/a&gt; in resource_metrics.go). When to use which for per-Pod usage is covered in Align memory request with actual usage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important thing here is the definition of the working set.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;container_memory_usage_bytes&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;container_memory_total_inactive_file_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In other words, &lt;strong&gt;only &lt;code&gt;inactive_file&lt;/code&gt; is subtracted from usage, with the result clamped to zero; active page cache remains in the working set&lt;/strong&gt;. For the implementation see cAdvisor's &lt;a href="https://github.com/google/cadvisor/blob/lib/v0.60.5/lib/container/libcontainer/handler.go#L890-L899" rel="noopener noreferrer"&gt;handler.go&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="n"&gt;workingSet&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Usage&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemoryStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inactiveFileKeyName&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalInactiveFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;workingSet&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;workingSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;workingSet&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkingSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workingSet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;As a breakdown, it is roughly the sum of the following. RSS (&lt;code&gt;container_memory_rss&lt;/code&gt;) is exactly &lt;code&gt;anon&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt; under cgroup v2, that is, the amount of anonymous pages, so it is included in the working set.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container_memory_working_set_bytes
  ≒ container_memory_rss                        (anonymous pages; shmem not included)
  + shmem                                       (tmpfs and shared memory; stays in full)
  + container_memory_total_active_file_bytes    (active page cache)
  + unreclaimable pages, kernel memory, etc.    (unevictable, slab, socket buffers, ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;shmem&lt;/code&gt; is listed as a separate term because &lt;strong&gt;neither &lt;code&gt;container_memory_rss&lt;/code&gt; (= &lt;code&gt;anon&lt;/code&gt; = &lt;code&gt;NR_ANON_MAPPED&lt;/code&gt;) nor &lt;code&gt;active_file&lt;/code&gt; includes &lt;code&gt;shmem&lt;/code&gt;&lt;/strong&gt;. As described above, &lt;code&gt;shmem&lt;/code&gt; is not on the file LRU, so it never appears in &lt;code&gt;active_file&lt;/code&gt; / &lt;code&gt;inactive_file&lt;/code&gt;, and since only &lt;code&gt;inactive_file&lt;/code&gt; is subtracted from the working set, it remains in full. No &lt;code&gt;container_*&lt;/code&gt; metric is exposed for &lt;code&gt;shmem&lt;/code&gt;, so this term can only be checked from &lt;code&gt;memory.stat&lt;/code&gt; on the node.&lt;/p&gt;

&lt;p&gt;The approximation reflects overlapping accounting categories and LRU classifications in &lt;code&gt;memory.stat&lt;/code&gt;. cAdvisor also exposes &lt;code&gt;container_memory_kernel_usage&lt;/code&gt;, but this alone cannot reconstruct the full breakdown. Read &lt;code&gt;memory.stat&lt;/code&gt; directly for more detail.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Caution: RSS in &lt;code&gt;container_memory_rss&lt;/code&gt; stands for Resident Set Size, but its scope differs from the real meaning.&lt;/strong&gt; True RSS means "the pages a process holds in physical memory" and includes &lt;code&gt;mmap&lt;/code&gt;ed file pages.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;memory.stat&lt;/code&gt; in cgroup v2 has no &lt;code&gt;rss&lt;/code&gt; field, and cAdvisor &lt;a href="https://github.com/google/cadvisor/blob/lib/v0.60.5/lib/container/libcontainer/handler.go#L830-L845" rel="noopener noreferrer"&gt;exposes &lt;code&gt;anon&lt;/code&gt; as &lt;code&gt;container_memory_rss&lt;/code&gt;&lt;/a&gt;. The kernel documentation defines &lt;code&gt;anon&lt;/code&gt; as "Amount of memory used in anonymous mappings such as brk(), sbrk(), and mmap(MAP_ANONYMOUS)", which does not include file mappings. So it does not match the &lt;code&gt;RSS&lt;/code&gt; column of &lt;code&gt;ps&lt;/code&gt; or &lt;code&gt;VmRSS&lt;/code&gt; in &lt;code&gt;/proc/[pid]/status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; A large &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; does not necessarily mean the application needs that much memory. It may just be accumulated page cache. To see how much memory the application has actually allocated, compare against &lt;code&gt;container_memory_rss&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Node metrics
&lt;/h3&gt;

&lt;p&gt;There are two families of metrics representing whole-node memory usage, with different emitters.&lt;/p&gt;
&lt;h4&gt;
  
  
  From cAdvisor (the kubelet's view)
&lt;/h4&gt;

&lt;p&gt;cAdvisor exposes memory usage not only for containers but also for the root cgroup (&lt;code&gt;id="/"&lt;/code&gt;, the topmost cgroup that contains every process on the node, as described above) and for the cgroup of all Pods. The &lt;code&gt;id&lt;/code&gt; of the all-Pods cgroup depends on the cgroup driver, so the queries below match both values with &lt;code&gt;=~&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;container_memory_working_set_bytes{id="/"}&lt;/code&gt;: working set of the whole node (Pods + other processes). &lt;strong&gt;But the root cgroup has no &lt;code&gt;memory.current&lt;/code&gt;, so the backing value differs from containers&lt;/strong&gt; (see below)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_memory_working_set_bytes{id=~"/kubepods|/kubepods.slice"}&lt;/code&gt;: working set of all Pods on the node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_memory_total_active_file_bytes{id="/"}&lt;/code&gt;: active page cache of the whole node&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both the &lt;code&gt;MEMORY(bytes)&lt;/code&gt; column of &lt;code&gt;kubectl top node&lt;/code&gt; and the kubelet's eviction decision described below use this working set.&lt;/p&gt;
&lt;h4&gt;
  
  
  From node-exporter (&lt;code&gt;/proc/meminfo&lt;/code&gt; as is)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/prometheus/node_exporter" rel="noopener noreferrer"&gt;node-exporter&lt;/a&gt; turns the contents of &lt;code&gt;/proc/meminfo&lt;/code&gt; directly into metrics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node_memory_MemTotal_bytes&lt;/code&gt;: total physical memory on the node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt;: the kernel's estimate of "how much can be allocated when new memory is requested"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node_memory_Cached_bytes&lt;/code&gt;: page cache size. As the kernel documentation says, "In-memory cache for files read from the disk (the pagecache) as well as tmpfs &amp;amp; shmem", it includes tmpfs and shared memory. The breakdown is in &lt;code&gt;node_memory_Shmem_bytes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt; &lt;strong&gt;counts reclaimable page cache as free&lt;/strong&gt;. Computing node memory utilization as &lt;code&gt;1 - MemAvailable / MemTotal&lt;/code&gt; is also in node-exporter's official mixin as the &lt;code&gt;NodeMemoryHighUtilization&lt;/code&gt; alert (&lt;a href="https://github.com/prometheus/node_exporter/blob/v1.12.1/docs/node-mixin/alerts/alerts.libsonnet#L364-L367" rel="noopener noreferrer"&gt;alerts.libsonnet&lt;/a&gt;). Its &lt;code&gt;expr&lt;/code&gt; uses variables for the selector and threshold; stripped of those it is &lt;code&gt;100 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100) &amp;gt; &amp;lt;threshold&amp;gt;&lt;/code&gt;. However, as shown below, this value does not match the utilization the kubelet uses for eviction decisions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Values the kubelet uses for eviction decisions
&lt;/h3&gt;

&lt;p&gt;The kubelet normally evaluates memory availability every 10 seconds. Crossing a threshold contributes to &lt;code&gt;MemoryPressure&lt;/code&gt;; eviction also depends on soft-threshold grace periods and reclaim handling. This &lt;code&gt;memory.available&lt;/code&gt; is not &lt;code&gt;/proc/meminfo&lt;/code&gt; &lt;code&gt;MemAvailable&lt;/code&gt;: &lt;strong&gt;it is computed from the root cgroup's working set&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memory.available = node_memory_MemTotal_bytes - container_memory_working_set_bytes{id="/"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the implementation see Kubernetes' &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/stats/helper.go#L63-L87" rel="noopener noreferrer"&gt;helper.go&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/eviction/helpers_others.go#L27-L37" rel="noopener noreferrer"&gt;helpers_others.go&lt;/a&gt;. The 10-second interval is defined as &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/kubelet.go#L197" rel="noopener noreferrer"&gt;&lt;code&gt;evictionMonitoringPeriod&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning: The root cgroup working set used for eviction decisions (and by &lt;code&gt;kubectl top node&lt;/code&gt;) is not the same thing as a container's working set.&lt;/strong&gt; In cgroup v2, &lt;code&gt;memory.current&lt;/code&gt; has the &lt;code&gt;CFTYPE_NOT_ON_ROOT&lt;/code&gt; flag, so &lt;strong&gt;the root cgroup has no &lt;code&gt;memory.current&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;memory_files[]&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/memcontrol.c#L4350-L4354" rel="noopener noreferrer"&gt;mm/memcontrol.c&lt;/a&gt;).&lt;/p&gt;


&lt;pre class="highlight c"&gt;&lt;code&gt;  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CFTYPE_NOT_ON_ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_u64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_current_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;code&gt;memory.stat&lt;/code&gt;, on the other hand, has no such flag, so it can be read even on the root. The cgroup library cAdvisor uses therefore substitutes &lt;code&gt;anon + file&lt;/code&gt; from &lt;code&gt;memory.stat&lt;/code&gt; as the usage when reading &lt;code&gt;memory.current&lt;/code&gt; fails with &lt;code&gt;ENOENT&lt;/code&gt;, for the root only (&lt;code&gt;rootStatsFromMeminfo()&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/vendor/github.com/opencontainers/cgroups/fs2/memory.go#L224-L231" rel="noopener noreferrer"&gt;opencontainers/cgroups&lt;/a&gt;).&lt;/p&gt;


&lt;pre class="highlight go"&gt;&lt;code&gt;  &lt;span class="c"&gt;// sum `anon` + `file` to report the same value as `usage_in_bytes` in v1.&lt;/span&gt;
  &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemoryStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Usage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemoryStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"anon"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemoryStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Kubernetes v1.37.1 selects &lt;code&gt;opencontainers/cgroups&lt;/code&gt; v0.0.7. For a real host root cgroup, the &lt;code&gt;id="/"&lt;/code&gt; series is therefore:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;container_memory_usage_bytes&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; anon + file
container_memory_working_set_bytes&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; anon + file - inactive_file
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Unlike &lt;code&gt;memory.current&lt;/code&gt;, this includes no kernel memory at all, such as slab, &lt;code&gt;pagetables&lt;/code&gt;, &lt;code&gt;kernel_stack&lt;/code&gt;, &lt;code&gt;percpu&lt;/code&gt; and &lt;code&gt;sock&lt;/code&gt;.&lt;/strong&gt; This is mainly why the node working set tends to come out smaller than usage derived from &lt;code&gt;/proc/meminfo&lt;/code&gt;. For container and Pod cgroups &lt;code&gt;memory.current&lt;/code&gt; can be read directly, so this substitution does not occur.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the working set includes active page cache, &lt;strong&gt;the kubelet treats active page cache as "memory in use"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For memory on Linux, the default is the hard-eviction threshold &lt;code&gt;memory.available&amp;lt;100Mi&lt;/code&gt;, as in &lt;code&gt;DefaultEvictionHard&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/eviction/defaults_linux.go#L21-L28" rel="noopener noreferrer"&gt;&lt;code&gt;defaults_linux.go&lt;/code&gt;&lt;/a&gt;; no soft eviction is configured by default.&lt;/p&gt;

&lt;p&gt;Choose thresholds appropriate to the node size and workload. In practice, though, I think it is better to override this value explicitly. In the v1.37 cluster I used for verification I also pass the following flags to the kubelet to set both hard and soft thresholds.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--eviction-hard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory.available&amp;lt;5%,nodefs.available&amp;lt;5%,pid.available&amp;lt;5%
&lt;span class="nt"&gt;--eviction-soft&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory.available&amp;lt;10%,nodefs.available&amp;lt;10%,pid.available&amp;lt;10%
&lt;span class="nt"&gt;--eviction-soft-grace-period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory.available&lt;span class="o"&gt;=&lt;/span&gt;2m,nodefs.available&lt;span class="o"&gt;=&lt;/span&gt;5s,pid.available&lt;span class="o"&gt;=&lt;/span&gt;1m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The default &lt;code&gt;100Mi&lt;/code&gt; is a fixed value, so &lt;strong&gt;it becomes relatively thinner on nodes with more memory&lt;/strong&gt;. If you specify percentages, the threshold follows the node size. Placing soft eviction ahead of hard (10 %) and giving it a grace period with &lt;code&gt;--eviction-soft-grace-period&lt;/code&gt; avoids evicting Pods on momentary spikes while still evicting them when the pressure persists.&lt;/p&gt;

&lt;p&gt;You can check your cluster's settings with the following query.&lt;/p&gt;

&lt;p&gt;Setting only some &lt;code&gt;evictionHard&lt;/code&gt; signals does not inherit the unspecified defaults unless &lt;code&gt;mergeDefaultEvictionSettings&lt;/code&gt; is enabled. Include all required disk and memory signals or check that setting.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="s2"&gt;"/api/v1/nodes/&amp;lt;node name&amp;gt;/proxy/configz"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.kubeletconfig | {evictionHard, evictionSoft, evictionSoftGracePeriod}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evictionHard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memory.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nodefs.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pid.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5%"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evictionSoft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memory.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nodefs.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pid.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10%"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evictionSoftGracePeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memory.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"nodefs.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pid.available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1m"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The denominator is Capacity (&lt;code&gt;node.status.capacity.memory&lt;/code&gt;, &lt;code&gt;kube_node_status_capacity{resource="memory"}&lt;/code&gt;), not Allocatable. These normally match &lt;code&gt;node_memory_MemTotal_bytes&lt;/code&gt;; verify that the metrics describe the same host and cgroup namespace. The kubelet uses cAdvisor's value as the machine's memory capacity, and cAdvisor reads it from &lt;code&gt;MemTotal&lt;/code&gt; in &lt;code&gt;/proc/meminfo&lt;/code&gt; (&lt;a href="https://github.com/google/cadvisor/blob/lib/v0.60.5/lib/container/raw/handler.go#L137-L146" rel="noopener noreferrer"&gt;raw/handler.go&lt;/a&gt;). A threshold like &lt;code&gt;memory.available&amp;lt;5%&lt;/code&gt; is also evaluated as a percentage of this Capacity.&lt;/p&gt;

&lt;p&gt;Allocatable is Capacity minus the system reservation and the hard eviction threshold, and is the upper bound the scheduler uses when assigning Pod requests. It is not used to decide whether eviction happens. See &lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/" rel="noopener noreferrer"&gt;Reserve Compute Resources for System Daemons&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Why node-exporter and kubelet usage disagree
&lt;/h3&gt;

&lt;p&gt;Putting the above together, there are two views of the same node.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;View&lt;/th&gt;
&lt;th&gt;Calculation&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Active page cache&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node memory utilization from node-exporter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1 - MemAvailable / MemTotal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;node-exporter (&lt;code&gt;/proc/meminfo&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The part estimated reclaimable counts as free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubelet's eviction decision&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node working set / MemTotal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet; root cgroup &lt;code&gt;anon + file - inactive_file&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Counted entirely as in use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;MemAvailable&lt;/code&gt; is the kernel's estimate of "how much can be newly allocated", and it adds part of the page cache and the reclaimable slab to free memory. It does not count the whole page cache as free. The implementation is &lt;code&gt;si_mem_available()&lt;/code&gt; in &lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/show_mem.c#L43-L67" rel="noopener noreferrer"&gt;mm/show_mem.c&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="cm"&gt;/*
     * Estimate the amount of memory available for userspace allocations,
     * without causing swapping or OOM.
     */&lt;/span&gt;
    &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_zone_page_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NR_FREE_PAGES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;totalreserve_pages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/*
     * Not all the page cache can be freed, otherwise the system will
     * start swapping or thrashing. Assume at least half of the page
     * cache, or the low watermark worth of cache, needs to stay.
     */&lt;/span&gt;
    &lt;span class="n"&gt;pagecache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_node_page_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NR_ACTIVE_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;global_node_page_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NR_INACTIVE_FILE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;pagecache&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pagecache&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wmark_low&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;pagecache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/*
     * Part of the reclaimable slab and other kernel memory consists of
     * items that are in use, and cannot be freed. Cap this estimate at the
     * low watermark.
     */&lt;/span&gt;
    &lt;span class="n"&gt;reclaimable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_node_page_state_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NR_SLAB_RECLAIMABLE_B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;global_node_page_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NR_KERNEL_MISC_RECLAIMABLE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;reclaimable&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reclaimable&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wmark_low&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;reclaimable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three things can be read from this.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is subtracted is &lt;code&gt;min(half, watermark_low)&lt;/code&gt;, so &lt;strong&gt;at least half of the page cache and the reclaimable slab is counted as free&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;From the free pages, &lt;code&gt;totalreserve_pages&lt;/code&gt; is subtracted, not &lt;code&gt;watermark_low&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The page cache term is the file LRU (&lt;code&gt;NR_ACTIVE_FILE + NR_INACTIVE_FILE&lt;/code&gt;), so &lt;strong&gt;&lt;code&gt;shmem&lt;/code&gt;, which sits on the anon LRU, is never counted as free&lt;/strong&gt;. On nodes that use tmpfs heavily, &lt;code&gt;MemAvailable&lt;/code&gt; also comes out small&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is that &lt;strong&gt;the definitions differ, and the size and direction of the difference depend on the environment&lt;/strong&gt;. There are four factors behind the divergence, and they push in opposite directions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Which utilization comes out higher&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The working set counts &lt;code&gt;active_file&lt;/code&gt; entirely as "in use"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The kubelet's utilization is higher&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The working set removes &lt;code&gt;inactive_file&lt;/code&gt; entirely as "free"&lt;/td&gt;
&lt;td&gt;node-exporter's utilization is higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MemAvailable&lt;/code&gt; subtracts &lt;code&gt;min(half, watermark_low)&lt;/code&gt; from file-LRU cache before counting it as available (&lt;code&gt;shmem&lt;/code&gt; is excluded)&lt;/td&gt;
&lt;td&gt;node-exporter's utilization is higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The root's usage is computed as &lt;code&gt;anon + file&lt;/code&gt;, so it includes no kernel memory such as slab or pagetables&lt;/td&gt;
&lt;td&gt;node-exporter's utilization is higher&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These rows are not independent additive terms; some describe the same cache treatment from different angles. Compare statistics from the same time rather than counting how many factors favor either side.&lt;/p&gt;

&lt;p&gt;The kubelet side is higher on nodes running workloads where &lt;code&gt;active_file&lt;/code&gt; builds up easily, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch jobs and ETL that read and write large files&lt;/li&gt;
&lt;li&gt;Services that emit a lot of logs (stdout is written to files on the node by the container runtime, so it becomes page cache)&lt;/li&gt;
&lt;li&gt;Middleware designed around the page cache (Kafka, Elasticsearch, etc.)&lt;/li&gt;
&lt;li&gt;Log collection agents that keep reading log files on the node&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What matters is the amount of file access, not the language or runtime. For example, a JVM heap is anonymous pages, so it raises RSS, not &lt;code&gt;active_file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On such a node it can happen that utilization computed from &lt;code&gt;MemAvailable&lt;/code&gt; looks like 33 % while the kubelet's side has reached 53 %. &lt;strong&gt;If you look at the &lt;code&gt;MemAvailable&lt;/code&gt;-based utilization and conclude there is plenty of room, the kubelet may in fact be approaching its threshold and evictions will occur.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the other hand, on a node where page cache has not built up much:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MemTotal          3.820 GiB
MemAvailable      2.692 GiB   →  1 - 2.692/3.820 = 29.6 %  (node-exporter side)
usage(/)          2.557 GiB   (= anon + file of the root)
inactive_file(/)  1.622 GiB
active_file(/)    0.227 GiB
Working Set(/)    0.935 GiB   →  0.935/3.820     = 24.5 %  (kubelet side)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;active_file&lt;/code&gt; is only 0.23 GiB while &lt;code&gt;inactive_file&lt;/code&gt; is 1.6 GiB, so the working set removes it all as "free", and the kubelet side comes out about 5 points lower. &lt;strong&gt;The kubelet side is not always higher.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Either way, the conclusion is that &lt;strong&gt;you cannot judge whether there is headroom from only one of the values&lt;/strong&gt;. &lt;code&gt;active_file&lt;/code&gt; is an auxiliary indicator for confirming the cause of the divergence, and its value is not itself the difference. When investigating node memory pressure, also check the kubelet-view utilization with the following PromQL.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Node memory utilization from the kubelet's view (the value used in eviction decisions)
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;group_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;kubelet_node_name&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kube_node_status_capacity&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Amount of active page cache, the main cause of the divergence
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_total_active_file_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;group_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;kubelet_node_name&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The label alignment is there because &lt;strong&gt;no cAdvisor &lt;code&gt;container_*&lt;/code&gt; metric carries any label identifying the node&lt;/strong&gt;. Looking directly at the kubelet's &lt;code&gt;/metrics/cadvisor&lt;/code&gt;, the root cgroup series looks like this:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;container_memory_working_set_bytes&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;,id&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;,image&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;,name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;,namespace&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;,pod&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 8.92522496e+08
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The only thing that tells you the node is the &lt;code&gt;instance&lt;/code&gt; label Prometheus adds at scrape time, and labels such as &lt;code&gt;kubernetes_io_hostname&lt;/code&gt; are added by your Prometheus relabel configuration. &lt;strong&gt;Because the name depends on the relabel configuration (or may not exist at all), referring to it with &lt;code&gt;label_replace&lt;/code&gt; is environment specific.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the queries above use &lt;code&gt;kubelet_node_name&lt;/code&gt;, which the kubelet exposes on &lt;code&gt;/metrics&lt;/code&gt;.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;kubelet_node_name&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"memqos-control-plane"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This is a gauge whose value is always &lt;code&gt;1&lt;/code&gt;, and &lt;strong&gt;the node name is in the &lt;code&gt;node&lt;/code&gt; label&lt;/strong&gt;. The value itself is meaningless; it is the &lt;strong&gt;info metric&lt;/strong&gt; pattern, which carries the information in labels. The kubelet's help text says the same.&lt;/p&gt;


&lt;pre class="highlight go"&gt;&lt;code&gt;  &lt;span class="c"&gt;// NodeName is a Gauge that tracks the node's name. The count is always 1.&lt;/span&gt;
  &lt;span class="n"&gt;NodeName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewGaugeVec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GaugeOpts&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;Subsystem&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KubeletSubsystem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;NodeNameKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;"The node's name. The count is always 1."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;In OpenMetrics, &lt;code&gt;Info&lt;/code&gt; is defined as an independent metric type, with a &lt;strong&gt;mandatory &lt;code&gt;_info&lt;/code&gt; suffix and a sample value that is always &lt;code&gt;1&lt;/code&gt;&lt;/strong&gt; (&lt;a href="https://prometheus.io/docs/specs/om/open_metrics_spec/#suffixes" rel="noopener noreferrer"&gt;OpenMetrics spec - Suffixes&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Info metrics are used to expose textual information which SHOULD NOT change during process lifetime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;kubelet_node_name&lt;/code&gt; has no &lt;code&gt;_info&lt;/code&gt; suffix and is exposed as a gauge, so &lt;strong&gt;strictly speaking it is not an OpenMetrics &lt;code&gt;Info&lt;/code&gt; type&lt;/strong&gt;. But "value always 1, information in labels" is the same usage, so this article treats it as an info-metric pattern. Similar examples are kube-state-metrics' &lt;code&gt;kube_node_info&lt;/code&gt; and node-exporter's &lt;code&gt;node_uname_info&lt;/code&gt;. When both endpoints are scraped with matching target labels, &lt;code&gt;instance&lt;/code&gt; matches, and &lt;code&gt;* on(instance) group_left(node)&lt;/code&gt; brings in the &lt;code&gt;node&lt;/code&gt; label. And the resulting &lt;code&gt;node&lt;/code&gt; label has the same name as kube-state-metrics' &lt;code&gt;node&lt;/code&gt; label, so they join directly.&lt;/p&gt;

&lt;p&gt;If your environment adds its own node label by relabeling, you can use &lt;code&gt;label_replace&lt;/code&gt; as follows (replace &lt;code&gt;kubernetes_io_hostname&lt;/code&gt; with the value in your environment).&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;max by &lt;span class="o"&gt;(&lt;/span&gt;node&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;
  label_replace&lt;span class="o"&gt;(&lt;/span&gt;
    container_memory_working_set_bytes&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"node"&lt;/span&gt;, &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;, &lt;span class="s2"&gt;"kubernetes_io_hostname"&lt;/span&gt;, &lt;span class="s2"&gt;"(.*)"&lt;/span&gt;
  &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Eviction candidates are not limited to Pods in particular namespaces. When node memory gets tight, Pods in every namespace on that node are candidates. Which Pod is chosen is decided in this order: (1) whether usage exceeds the memory request, (2) Pod priority, (3) how far above the request it is. See &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#pod-selection-for-kubelet-eviction" rel="noopener noreferrer"&gt;Pod selection for kubelet eviction&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;critical Pods are exempt from node-pressure eviction by the kubelet&lt;/strong&gt; (&lt;code&gt;evictPod()&lt;/code&gt; in &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/eviction/eviction_manager.go#L633-L640" rel="noopener noreferrer"&gt;eviction_manager.go&lt;/a&gt;). "Critical pod" is a term in the official Kubernetes documentation and refers to a Pod whose PriorityClass is &lt;code&gt;system-cluster-critical&lt;/code&gt; or &lt;code&gt;system-node-critical&lt;/code&gt; (&lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/" rel="noopener noreferrer"&gt;Guaranteed Scheduling For Critical Add-On Pods&lt;/a&gt; says "To mark a Pod as critical, set priorityClassName for that Pod to &lt;code&gt;system-cluster-critical&lt;/code&gt; or &lt;code&gt;system-node-critical&lt;/code&gt;.").&lt;/p&gt;


&lt;pre class="highlight go"&gt;&lt;code&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kubelettypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCriticalPod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Eviction manager: cannot evict a critical pod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;klog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KObj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The kubelet's check is broader than this definition: &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/types/pod_update.go#L157-L169" rel="noopener noreferrer"&gt;&lt;code&gt;IsCriticalPod()&lt;/code&gt;&lt;/a&gt; is also true for static Pods and mirror Pods. The remaining condition is that &lt;code&gt;pod.Spec.Priority&lt;/code&gt; is at least &lt;code&gt;SystemCriticalPriority&lt;/code&gt; (= &lt;code&gt;2 × 1000000000&lt;/code&gt;), and since the upper limit of user-definable priority is &lt;code&gt;HighestUserDefinablePriority&lt;/code&gt; (= &lt;code&gt;1000000000&lt;/code&gt;), the only PriorityClasses that match are the two above. &lt;strong&gt;These priority classes are exempt from node-pressure eviction, but that exemption does not itself determine their protection during global OOM.&lt;/strong&gt; See Which process gets killed for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Once the cause is clear, review requests and placement for node pressure, and usage and limits for memcg OOM.&lt;/p&gt;
&lt;h3&gt;
  
  
  Start with the limiting cgroup
&lt;/h3&gt;

&lt;p&gt;For memcg OOM, identify the cgroup named by &lt;code&gt;oom_memcg&lt;/code&gt; before changing resources. It may be the container, its Pod or an ancestor such as &lt;code&gt;/kubepods.slice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Compare &lt;code&gt;memory.current&lt;/code&gt; with that cgroup's &lt;code&gt;memory.max&lt;/code&gt;, then inspect &lt;code&gt;memory.stat&lt;/code&gt; to see whether anonymous memory, &lt;code&gt;shmem&lt;/code&gt;, file cache or kernel memory dominates. Reduce the required memory or adjust the relevant limit after checking node capacity. Raising a request alone does not raise that limit.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files" rel="noopener noreferrer"&gt;cgroup v2 memory interfaces&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Align memory request with actual usage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Aligning memory requests with actual usage helps prevent excessive placement density on a node.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With the default v1.37 kubelet configuration, &lt;code&gt;resources.requests.memory&lt;/code&gt; is not applied as cgroup memory protection, so it does not directly limit how much memory a container can use. It nevertheless matters because the request determines how likely OOM kills and evictions are, in these three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Placement (how likely both global OOM and eviction are)&lt;/strong&gt;: the standard resource-fit check uses requests and Allocatable rather than actual usage. Affinity, taints and other rules also affect placement. A Pod with no request, or one smaller than actual usage, is placed while overestimating the node's free memory. As a result, Pods with large memory usage concentrate on the same node and the whole node becomes tight, &lt;strong&gt;inviting both global OOM and eviction&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction candidate selection&lt;/strong&gt;: the kubelet preferentially evicts Pods whose actual usage exceeds their memory request. If you set the request smaller than actual usage, that Pod is evicted more easily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;oom_score_adj&lt;/code&gt; (the order of kills in a global OOM)&lt;/strong&gt;: as described earlier, the Burstable &lt;code&gt;oom_score_adj&lt;/code&gt; is &lt;code&gt;1000 - 1000 × memory request / node memory capacity&lt;/code&gt;. &lt;strong&gt;The smaller the request, the larger the &lt;code&gt;oom_score_adj&lt;/code&gt;, and the more likely the Pod is chosen as a kill target in a global OOM&lt;/strong&gt;. BestEffort Pods receive &lt;code&gt;1000&lt;/code&gt;. An unset memory request alone does not imply BestEffort: CPU resources can make a Pod Burstable, and a memory limit can default its request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"Whether the event happens" and "which Pod is chosen" are separate matters&lt;/strong&gt;, so organized by effect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the request acts&lt;/th&gt;
&lt;th&gt;memcg OOM occurs&lt;/th&gt;
&lt;th&gt;global OOM occurs&lt;/th&gt;
&lt;th&gt;global OOM target selection&lt;/th&gt;
&lt;th&gt;Eviction occurs&lt;/th&gt;
&lt;th&gt;Eviction target selection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Placement (scheduler)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oom_score_adj&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eviction candidate selection&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Only placement affects whether an event occurs&lt;/strong&gt;, and it affects both global OOM and eviction, because both start from node pressure. The effect is indirect, though: the request does not appear in the computation of &lt;code&gt;memory.available&lt;/code&gt; (the kubelet's memory-availability calculation), so raising a request does not relax the threshold. The node just becomes less likely to get tight as a result of lower placement density.&lt;/p&gt;

&lt;p&gt;The other two only affect &lt;strong&gt;whether your Pod is chosen&lt;/strong&gt; after the pressure has built up. With the default settings, increasing a request does not raise &lt;code&gt;memory.max&lt;/code&gt; or resolve a memcg limit violation.&lt;/p&gt;

&lt;p&gt;Raising requests can reduce placement density and alter victim selection. It can also leave Pods Pending or trigger node expansion, so account for capacity and cost.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Increasing a request alone does not resolve memcg OOM. Reduce usage or review the limiting cgroup's limit after checking node capacity. If Memory QoS protection or throttling is configured, requests also affect reclaim behavior. This section focuses on reducing global OOM and eviction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  PromQL for comparing requests and actual usage
&lt;/h4&gt;

&lt;p&gt;You can check whether a request matches actual usage with PromQL. I included queries for the per-Pod ratio, for finding Pods with no request, for per-node comparison, and for deriving a recommended value from past usage. &lt;strong&gt;I also explain the pitfalls that come from where the metrics originate (such as double counting).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  PromQL for checking and notes on filters
  &lt;p&gt;&lt;strong&gt;What to know before comparing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These queries assume one cluster and that the required metrics are collected. For multiple clusters, filter by cluster or include a cluster label in every aggregation and join key. When joining on &lt;code&gt;instance&lt;/code&gt;, restrict jobs as needed so that the right-hand &lt;code&gt;kubelet_node_name&lt;/code&gt; has one series per instance.&lt;/p&gt;

&lt;p&gt;A Pod's request is available as &lt;code&gt;kube_pod_resource_request&lt;/code&gt;, which kube-scheduler exposes on its &lt;code&gt;/metrics/resources&lt;/code&gt; endpoint. You compare it with actual usage (&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;), but the two come from different sources, so dividing them as is gives the wrong result. Keep these five points in mind.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Point&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scrape configuration is needed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kube_pod_resource_request&lt;/code&gt; is on kube-scheduler's secure port (default 10259) at &lt;code&gt;/metrics/resources&lt;/code&gt;. It is a different endpoint from &lt;code&gt;/metrics&lt;/code&gt;, so Prometheus needs explicit configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No series for Pods with no request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Because the collector does &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/scheduler/metrics/resources/resources.go#L162-L177" rel="noopener noreferrer"&gt;&lt;code&gt;if val.IsZero() { return }&lt;/code&gt;&lt;/a&gt;, Pods whose request is 0 or unset do not appear in the metric. &lt;strong&gt;Pods with zero requests drop out of the ratio query&lt;/strong&gt;, so find them separately with &lt;code&gt;unless&lt;/code&gt; below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregated per Pod&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kube_pod_resource_request&lt;/code&gt; is the total for the whole Pod and has no &lt;code&gt;container&lt;/code&gt; label. Actual usage must also be aligned to Pod level before comparing (how is shown below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate scrapes&lt;/td&gt;
&lt;td&gt;kube-scheduler runs multiple replicas in an HA setup, so the same Pod's series may be scraped several times with different &lt;code&gt;instance&lt;/code&gt; values. Summing them double counts, so &lt;strong&gt;normalize with &lt;code&gt;max by (...)&lt;/code&gt;&lt;/strong&gt; before aggregating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminated Pods are excluded&lt;/td&gt;
&lt;td&gt;Pods that are Succeeded / Failed are not included&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Which metric gives per-Pod memory usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kube_pod_resource_request&lt;/code&gt; is a per-Pod value, so actual usage must also be aligned to Pod level. You may wonder "doesn't kube-state-metrics have a Pod memory usage metric?", but &lt;strong&gt;it does not&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;kube-state-metrics converts Kubernetes API objects (spec and status) directly into metrics and does not handle actual resource usage at all. It provides &lt;strong&gt;configured values written in manifests&lt;/strong&gt;, such as &lt;code&gt;kube_pod_container_resource_requests&lt;/code&gt; / &lt;code&gt;kube_pod_container_resource_limits&lt;/code&gt;, but usage is out of scope. Usage requires reading cgroups, which is the job of cAdvisor and the kubelet.&lt;/p&gt;

&lt;p&gt;There are three ways to get actual per-Pod usage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Characteristics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sum the containers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sum by (namespace, pod) (container_memory_working_set_bytes{...})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (&lt;code&gt;/metrics/cadvisor&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Works when the required container metrics are collected. Double counts if the filter is wrong (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Look at the Pod's cgroup directly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;container_memory_working_set_bytes{container="", image="", pod!=""}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (&lt;code&gt;/metrics/cadvisor&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No summing needed. Slightly larger than the container total because it includes the pause container and Pod-level cgroup charges. &lt;code&gt;image=""&lt;/code&gt; is required (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The kubelet's per-Pod metric&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pod_memory_working_set_bytes{namespace, pod}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kubelet &lt;code&gt;/metrics/resource&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The most straightforward, but it is on a different path from &lt;code&gt;/metrics/cadvisor&lt;/code&gt;, so you must add a scrape configuration to Prometheus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the default cAdvisor statistics provider, the second and third use the same Pod cgroup. Sampling times and CRI statistics settings can produce differences. The kubelet gets &lt;code&gt;pod_memory_working_set_bytes&lt;/code&gt; &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/stats/cadvisor_stats_provider.go#L156-L168" rel="noopener noreferrer"&gt;from the Pod-level cgroup&lt;/a&gt;, not as a sum of containers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;        &lt;span class="n"&gt;podUID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;podStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PodRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c"&gt;// Lookup the pod-level cgroup's CPU and memory stats&lt;/span&gt;
        &lt;span class="n"&gt;podInfo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getCadvisorPodInfoFromPodUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;podUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allInfos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;podInfo&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cadvisorInfoToCPUandMemoryStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;podInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;podStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CPU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cpu&lt;/span&gt;
            &lt;span class="n"&gt;podStats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pod_memory_working_set_bytes&lt;/code&gt; is a STABLE metric, so if Prometheus scrapes &lt;code&gt;/metrics/resource&lt;/code&gt; in your environment, using it is the simplest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Actual per-Pod usage (when scraping the kubelet's /metrics/resource)
&lt;/span&gt;&lt;span class="n"&gt;pod_memory_working_set_bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do not scrape &lt;code&gt;/metrics/resource&lt;/code&gt;, specify the Pod cgroup series directly. &lt;code&gt;/metrics/resource&lt;/code&gt; is a different path from &lt;code&gt;/metrics/cadvisor&lt;/code&gt;, so it is not collected unless you add it to the Prometheus configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Actual per-Pod usage (looking at the Pod's cgroup series directly)
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;container=""&lt;/code&gt; gets the Pod's cgroup because when the kubelet labels metrics it &lt;strong&gt;also attaches &lt;code&gt;pod&lt;/code&gt; and &lt;code&gt;namespace&lt;/code&gt; to the Pod's cgroup series&lt;/strong&gt;. The code explains it as "Associate pod cgroup with pod so we have an accurate accounting of sandbox" (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/server/server.go#L1481-L1495" rel="noopener noreferrer"&gt;server.go&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning: Do not omit &lt;code&gt;image=""&lt;/code&gt;.&lt;/strong&gt; If you filter with only &lt;code&gt;container=""&lt;/code&gt;, &lt;strong&gt;the series of the pause container (sandbox) also matches&lt;/strong&gt; along with the Pod's cgroup. In containerd environments the pause container's &lt;code&gt;container&lt;/code&gt; label is an empty string and its &lt;code&gt;image&lt;/code&gt; label holds the pause image.&lt;/p&gt;

&lt;p&gt;For example, in a cluster running 100 Pods, the series count splits into these three kinds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Selector&lt;/th&gt;
&lt;th&gt;Series&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{container="", pod!=""}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Pod cgroups + pause containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{container="", image="", pod!=""}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pod cgroups only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{container="", image!="", pod!=""}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Pause containers only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;max by (namespace, pod)&lt;/code&gt; the Pod's cgroup is larger, so the correct value comes back, but with &lt;code&gt;sum by&lt;/code&gt; the pause part is added and it is double counted. For one Pod it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod cgroup                666,116,096   ← correct value
Sum of real containers     665,882,624
pause container                225,280
sum{container="",pod!=""}  666,341,376   ← Pod cgroup + pause, double counted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do &lt;code&gt;sum by (namespace, pod)&lt;/code&gt; without a &lt;code&gt;container&lt;/code&gt; filter, each container's share and the Pod cgroup's share are both added, so the value is almost doubled. When summing containers, narrow to real containers only, as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Actual per-Pod usage (when summing containers)
&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In containerd environments the pause container's &lt;code&gt;container&lt;/code&gt; is empty, so &lt;code&gt;container!=""&lt;/code&gt; alone excludes the Pod cgroup, the root cgroup and pause. &lt;code&gt;image!=""&lt;/code&gt; is added because series other than real containers have no &lt;code&gt;image&lt;/code&gt;, and it is insurance for environments where &lt;code&gt;container!=""&lt;/code&gt; alone leaves some series. With CRI-O the pause container appears as &lt;code&gt;container="POD"&lt;/code&gt;, so also add &lt;code&gt;container!="POD"&lt;/code&gt; there.&lt;/p&gt;

&lt;p&gt;The following queries use the Pod cgroup series; verify that it is collected with these labels in your environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare request and actual usage for each Pod&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This compares the observed working set with the request. A value above 1 means usage currently exceeds the request. Check history and the breakdown before changing it: a temporary peak or reclaimable cache does not by itself establish that the request is too small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ratio of observed working set to memory request (above 1 means usage exceeds the request)
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;kube_pod_resource_request&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see the difference in bytes, do the following. A positive value is "the amount by which the request is exceeded".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Amount over the request (bytes)
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;
&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;kube_pod_resource_request&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find Pods with no request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As noted above, Pods with no request do not appear in &lt;code&gt;kube_pod_resource_request&lt;/code&gt;, so they do not show up in the ratio query. Use &lt;code&gt;unless&lt;/code&gt; to find "Pods that have a usage series but no request series". These may have a zero memory request, or their request series may be missing from collection. Check the scrape configuration and the applied Pod resources; CPU settings can make them Burstable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pods with no memory request, and their actual usage
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;unless&lt;/span&gt;
&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;kube_pod_resource_request&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Compare the sum of requests and the sum of actual usage per node&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For each node, compare "usage the scheduler knows about (sum of requests)" with "actual usage". &lt;strong&gt;A node where actual usage is larger is one where the scheduler overestimates free space&lt;/strong&gt;, and the risk of global OOM and eviction is high.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kube_pod_resource_request&lt;/code&gt; and &lt;code&gt;kube_node_status_allocatable&lt;/code&gt; both have a &lt;code&gt;node&lt;/code&gt; label, so these two can be joined without &lt;code&gt;label_replace&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Per node: sum of memory requests / Allocatable
&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;kube_pod_resource_request&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;kube_node_status_allocatable&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual-usage side comes from cAdvisor and has no &lt;code&gt;node&lt;/code&gt; label, so align labels as in the PromQL above. Using the working set of &lt;code&gt;/kubepods.slice&lt;/code&gt; gives the usage of all Pods on the node without summing per Pod.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Actual usage of all Pods on the node / Allocatable
&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=~&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/kubepods|/kubepods.slice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;group_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;kubelet_node_name&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;kube_node_status_allocatable&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put these two side by side and look for nodes where the lower query (actual usage) exceeds the upper one (requests).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derive a recommended request from past usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Decide the request not from an instantaneous value but from usage over a period. Anonymous memory required by the application cannot be time-shared like CPU. &lt;strong&gt;Use representative peaks as well as averages&lt;/strong&gt;, while separating reclaimable cache from the workload's persistent needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Maximum memory usage per Pod over the past 7 days (a guide for the request)
&lt;/span&gt;&lt;span class="nf"&gt;max_over_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do not want to be pulled around by transient spikes, use a quantile. Be aware, though, that if you use this value as the request, the Pod will exceed its request at peak times and become an eviction candidate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 95th percentile over the past 7 days
&lt;/span&gt;&lt;span class="nf"&gt;quantile_over_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;max&lt;/span&gt; &lt;span class="nf"&gt;by &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;container_memory_working_set_bytes&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The maximum obtained by this query &lt;strong&gt;can miss spikes shorter than the scrape interval or the subquery evaluation step (5 minutes here)&lt;/strong&gt;. For applications that temporarily allocate a lot of memory right after startup, the real peak may be higher than this value. It is the same reason as in My working set is below the limit, but the container was OOMKilled. Leave some margin, or decide after understanding the application's startup behavior.&lt;/p&gt;

&lt;p&gt;Also, the working set includes active page cache. For containers with a lot of logging or file I/O, using this value as is for the request makes it too large. Compare with &lt;code&gt;container_memory_rss&lt;/code&gt; and decide which to base it on.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Do not base request design on kubectl top
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;kubectl top pod&lt;/code&gt; is convenient, but it is not suited to designing requests. The reason lies in the design of Metrics Server itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Metrics Server gets from the kubelet is the value of the &lt;code&gt;/metrics/resource&lt;/code&gt; endpoint, and for memory it is &lt;strong&gt;the working set&lt;/strong&gt; (&lt;a href="https://github.com/kubernetes-sigs/metrics-server/blob/v0.8.1/FAQ.md" rel="noopener noreferrer"&gt;"Memory is reported as the working set at the instant the metric was collected"&lt;/a&gt;). It is the same value as &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; seen through Prometheus, so it includes page cache in the same way&lt;/li&gt;
&lt;li&gt;Metrics Server &lt;strong&gt;keeps only the latest value&lt;/strong&gt;. The binary default for &lt;code&gt;--metric-resolution&lt;/code&gt; is 60 seconds, while the v0.8.1 deployment manifest sets it to 15 seconds. Metrics Server keeps no long-term history, so it cannot show past peaks&lt;/li&gt;
&lt;li&gt;Metrics Server's own README states in its use cases &lt;em&gt;"Don't use Metrics Server when you need: ... An accurate source of resource usage metrics"&lt;/em&gt; (&lt;a href="https://github.com/kubernetes-sigs/metrics-server/blob/v0.8.1/README.md#use-cases" rel="noopener noreferrer"&gt;README.md&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;kubectl top&lt;/code&gt; to get a feel for "what is using a lot right now", and when you decide a request, specify a period and check with PromQL for comparing requests and actual usage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adjust requests automatically with VPA
&lt;/h3&gt;

&lt;p&gt;Instead of running the PromQL in the previous section by hand, you can have the &lt;a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler" rel="noopener noreferrer"&gt;Vertical Pod Autoscaler (VPA)&lt;/a&gt; compute recommendations. The idea is the same (derive from quantiles of past usage), and VPA runs it continuously, combined with OOM detection.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  VPA configuration examples and defaults
  &lt;p&gt;VPA consists of three components.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recommender&lt;/td&gt;
&lt;td&gt;Computes recommendations from usage history and writes them to &lt;code&gt;status.recommendation&lt;/code&gt; of the VPA object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updater&lt;/td&gt;
&lt;td&gt;Applies in-place updates or evicts Pods for recreation, depending on &lt;code&gt;updateMode&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admission Controller&lt;/td&gt;
&lt;td&gt;Rewrites &lt;code&gt;resources&lt;/code&gt; to the recommendation with a mutating webhook at Pod creation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Start by looking at recommendations with &lt;code&gt;updateMode: Off&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You would not want Pods to be rebuilt abruptly, so &lt;strong&gt;start with &lt;code&gt;updateMode: Off&lt;/code&gt; to inspect recommendations&lt;/strong&gt;. In this mode VPA does not touch Pods at all and only writes recommendations to the VPA object. The API definition also says "This can be used for a \"dry run\"".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VerticalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-vpa&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample&lt;/span&gt;
  &lt;span class="na"&gt;updatePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;updateMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Off"&lt;/span&gt;          &lt;span class="c1"&gt;# only compute recommendations. Pods are not changed&lt;/span&gt;
  &lt;span class="na"&gt;resourcePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containerPolicies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;controlledResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# default is ["cpu", "memory"]&lt;/span&gt;
      &lt;span class="na"&gt;controlledValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RequestsOnly&lt;/span&gt;    &lt;span class="c1"&gt;# default is RequestsAndLimits&lt;/span&gt;
      &lt;span class="na"&gt;minAllowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;
      &lt;span class="na"&gt;maxAllowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the recommendation as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe vpa sample-vpa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Recommendation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Container Recommendations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Container Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;Lower Bound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;262144k&lt;/span&gt;
      &lt;span class="na"&gt;Target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;367001600&lt;/span&gt;
      &lt;span class="na"&gt;Uncapped Target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;367001600&lt;/span&gt;
      &lt;span class="na"&gt;Upper Bound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;524288k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Target&lt;/code&gt; is the recommended request. &lt;code&gt;Lower Bound&lt;/code&gt; and &lt;code&gt;Upper Bound&lt;/code&gt; account for the history available and recommendation confidence; they do not guarantee the minimum runnable or maximum useful memory. The Updater uses these bounds when deciding whether a Pod needs an update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch to automatic application&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you confirm the recommendation is reasonable, change &lt;code&gt;updateMode&lt;/code&gt; to switch to automatic application. The modes are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;updateMode&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Off&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only computes recommendations. Pods are not changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Initial&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies recommendations &lt;strong&gt;only at Pod creation&lt;/strong&gt;. Running Pods are not changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Recreate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies at creation, and also updates running Pods by &lt;strong&gt;evicting and recreating&lt;/strong&gt; them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InPlaceOrRecreate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tries In-Place Resize first, and falls back to recreation if that is not possible. Requires the cluster's &lt;code&gt;InPlacePodVerticalScaling&lt;/code&gt; feature gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InPlace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tries only In-Place Resize and &lt;strong&gt;does not evict&lt;/strong&gt;. On failure it leaves it to the kubelet's retry. Requires VPA's own &lt;code&gt;InPlace&lt;/code&gt; feature gate in addition to the above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Deprecated&lt;/strong&gt;. Currently equivalent to &lt;code&gt;Recreate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Auto&lt;/code&gt; has been deprecated as of VPA 1.7, and the API comment also says "Use explicit update modes like \"Recreate\", \"Initial\", or \"InPlaceOrRecreate\" instead". &lt;strong&gt;Do not use &lt;code&gt;Auto&lt;/code&gt; in newly written manifests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a configuration example that automatically adjusts only the memory request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;autoscaling.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VerticalPodAutoscaler&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-vpa&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample&lt;/span&gt;
  &lt;span class="na"&gt;updatePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;updateMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recreate"&lt;/span&gt;
    &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;             &lt;span class="c1"&gt;# do not evict when replicas is below this (the global default is also 2)&lt;/span&gt;
  &lt;span class="na"&gt;resourcePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containerPolicies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;controlledResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;controlledValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RequestsOnly&lt;/span&gt;
      &lt;span class="na"&gt;minAllowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;
      &lt;span class="na"&gt;maxAllowed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;controlledValues: RequestsOnly&lt;/code&gt;, &lt;strong&gt;you can manage the limit yourself and leave only the request to VPA&lt;/strong&gt;. In the context of this article (reducing global OOM and eviction) this is the easy setting to work with. With the default &lt;code&gt;RequestsAndLimits&lt;/code&gt;, both are rewritten while keeping the ratio of request to limit, so note that the limit moves too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behavior when an OOM occurs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For memory, VPA has a mechanism that raises the recommendation when it detects an OOM kill. The defaults are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oomBumpUpRatio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When an OOM kill is detected, record usage at that time multiplied by 1.2 as a sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oomMinBumpUp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;100Mi&lt;/code&gt; (104857600)&lt;/td&gt;
&lt;td&gt;If the bump is smaller than this, bump up by at least this much&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memoryAggregationIntervalSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;86400&lt;/code&gt; (24 hours)&lt;/td&gt;
&lt;td&gt;Record one peak sample per this interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memoryAggregationIntervalCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many such intervals to keep. By default the memory recommendation is computed from 24 hours × 8 = &lt;strong&gt;8 days&lt;/strong&gt; of history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evictAfterOOMSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(unset)&lt;/td&gt;
&lt;td&gt;Make Pods that OOM within this many seconds of starting eviction targets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To use these per-container overrides in &lt;code&gt;containerPolicies&lt;/code&gt;, enable the &lt;code&gt;PerVPAConfig&lt;/code&gt; feature gate in the VPA components that handle them. In VPA 1.7.1 it is Alpha and disabled by default. See &lt;a href="https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/pkg/recommender/model/aggregate_container_state.go#L343-L358" rel="noopener noreferrer"&gt;the aggregation implementation&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;containerPolicies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;controlledResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;oomBumpUpRatio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.5"&lt;/span&gt;                    &lt;span class="c1"&gt;# bump up more strongly on OOM&lt;/span&gt;
      &lt;span class="na"&gt;oomMinBumpUp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
      &lt;span class="na"&gt;memoryAggregationIntervalSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;   &lt;span class="c1"&gt;# record a peak every hour&lt;/span&gt;
      &lt;span class="na"&gt;memoryAggregationIntervalCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;       &lt;span class="c1"&gt;# compute from 24 intervals = 1 day of history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The memory recommendation itself is computed from quantiles of a usage histogram. The Recommender defaults are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--target-memory-percentile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quantile used to compute &lt;code&gt;Target&lt;/code&gt; (the recommended request)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--recommendation-lower-bound-memory-percentile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quantile used to compute &lt;code&gt;Lower Bound&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--recommendation-upper-bound-memory-percentile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.95&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quantile used to compute &lt;code&gt;Upper Bound&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--recommendation-margin-fraction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Safety margin added to the computed value (15 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So by default, &lt;strong&gt;a 90th percentile from a weighted histogram of interval peaks, with a 15% margin and other bounds applied&lt;/strong&gt; becomes the recommended request. The idea is the same as the &lt;code&gt;quantile_over_time(0.95, ...)&lt;/code&gt; PromQL introduced in the previous section, and it is easiest to think of VPA as running that continuously, combined with OOM detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning: The usage VPA looks at is also the working set.&lt;/strong&gt; The Recommender gets usage from the Metrics Server API (&lt;code&gt;metrics.k8s.io/v1beta1&lt;/code&gt;), so as described earlier it derives recommendations from values that include page cache.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;for containers with a lot of logging or file I/O, reclaimable cache can increase VPA's recommendation&lt;/strong&gt;. For containers where the gap between &lt;code&gt;container_memory_rss&lt;/code&gt; and &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; is large, it may waste less in the end to cap with &lt;code&gt;maxAllowed&lt;/code&gt; or to decide by hand instead of leaving it to VPA.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;metrics.k8s.io&lt;/code&gt; became GA (&lt;code&gt;v1&lt;/code&gt;) in v1.37 through &lt;a href="https://github.com/kubernetes/enhancements/issues/5207" rel="noopener noreferrer"&gt;KEP-5207&lt;/a&gt;. Metrics Server v0.8.1, the version covered here, uses &lt;code&gt;v1beta1&lt;/code&gt;. The working-set definition does not depend on the API version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Operational points when using VPA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Combining with HPA&lt;/strong&gt;: &lt;strong&gt;do not use HPA and VPA on the same resource at the same time&lt;/strong&gt;. The official docs also state "should not be used with the HPA on the same resource metric (CPU or memory)" (&lt;a href="https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/known-limitations.md" rel="noopener noreferrer"&gt;known-limitations.md&lt;/a&gt;). If you split the targets they can be combined, and &lt;strong&gt;VPA for memory and HPA for CPU&lt;/strong&gt; is the officially recommended example. With &lt;code&gt;controlledResources: ["memory"]&lt;/code&gt; as in this article's examples, CPU can be left to HPA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Recreate&lt;/code&gt; rebuilds Pods&lt;/strong&gt;: the Updater evicts Pods using the Eviction API, so set a PodDisruptionBudget. It does not evict when replicas is below &lt;code&gt;minReplicas&lt;/code&gt; (the global &lt;code&gt;--min-replicas&lt;/code&gt;, default &lt;code&gt;2&lt;/code&gt;, if not specified in VPA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Place modes&lt;/strong&gt;: they can change the request without rebuilding the Pod, but require the cluster's &lt;code&gt;InPlacePodVerticalScaling&lt;/code&gt; feature gate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage collection is required&lt;/strong&gt;: this example uses Metrics Server; VPA can also be configured to use Prometheus for historical data&lt;/li&gt;
&lt;/ul&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  My working set is below the limit, but the container was OOMKilled
&lt;/h3&gt;

&lt;p&gt;What memcg OOM compares against the limit is &lt;code&gt;memory.current&lt;/code&gt; (&lt;code&gt;container_memory_usage_bytes&lt;/code&gt;), not the working set. Furthermore, &lt;code&gt;memory.current&lt;/code&gt; reaching the limit does not cause an OOM by itself; the kill happens when reclaim fails to bring usage back under the limit. Also, even if your container is under its limit, it can be killed in a node-wide memory exhaustion (global OOM). First use The two OOM kill paths to tell the path.&lt;/p&gt;

&lt;p&gt;If none of the values seems to have reached the limit, suspect a spike shorter than the scrape interval. A spike in memory usage shorter than Prometheus' scrape interval (the global &lt;code&gt;scrape_interval&lt;/code&gt; defaults to 1 minute; see &lt;a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/" rel="noopener noreferrer"&gt;Configuration&lt;/a&gt;) does not show up in metrics. Batch processing right after startup, or suddenly accepting a very large request, are examples.&lt;/p&gt;

&lt;p&gt;In that case, check values like these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kube-state-metrics&lt;/td&gt;
&lt;td&gt;The last termination reason. The value does not count up when OOMs repeat, so from the second time on, detect it by combining with the increase of &lt;code&gt;kube_pod_container_status_restarts_total&lt;/code&gt; (also kube-state-metrics)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_oom_events_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;Number of OOM kills. But &lt;strong&gt;the series disappears once the container cgroup is removed&lt;/strong&gt;. Detecting an increase depends on scraping it before removal; already collected historical samples remain in Prometheus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_vmstat_oom_kill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;node-exporter&lt;/td&gt;
&lt;td&gt;Number of OOM kills on the node. It cannot identify the Pod or container, but is not affected by container recreation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Besides metrics, also check &lt;code&gt;Last State&lt;/code&gt; in &lt;code&gt;kubectl describe pod&lt;/code&gt; (exit code 137 means termination by SIGKILL).&lt;/p&gt;
&lt;h3&gt;
  
  
  Node usage computed from MemAvailable is low, but Pods are evicted
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MemAvailable&lt;/code&gt; counts part of the page cache as "free", but the kubelet is working-set based and treats all of &lt;code&gt;active_file&lt;/code&gt; as "in use". With workloads that do a lot of file I/O or logging, &lt;code&gt;active_file&lt;/code&gt; can grow to several GiB, so even if the &lt;code&gt;MemAvailable&lt;/code&gt;-based utilization looks low, the kubelet side may be approaching its threshold.&lt;/p&gt;

&lt;p&gt;Conversely, on nodes where page cache has not built up much, node-exporter's side comes out higher. &lt;strong&gt;Which is higher depends on the environment&lt;/strong&gt;, so you cannot judge from just one of them. For the breakdown of factors and the PromQL to get the kubelet-side utilization, see Why node-exporter and kubelet usage disagree.&lt;/p&gt;
&lt;h3&gt;
  
  
  kubectl top pod and the Prometheus working set differ
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl top pod&lt;/code&gt; shows the sum of the containers' working sets obtained from the kubelet (via Metrics Server). Prometheus' &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; comes from the same source, but it is as stale as the scrape interval, so instantaneous values differ.&lt;/p&gt;
&lt;h3&gt;
  
  
  Memory working set keeps growing
&lt;/h3&gt;

&lt;p&gt;First check whether &lt;code&gt;container_memory_rss&lt;/code&gt; or &lt;code&gt;container_memory_cache&lt;/code&gt; is the one growing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;container_memory_rss&lt;/code&gt; is growing: suspect a memory leak in the application&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_memory_cache&lt;/code&gt; is growing: page cache is accumulating. But it includes not only the cache of files on disk but also tmpfs and shared memory. To tell which is growing, check &lt;code&gt;shmem&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt; on the node. tmpfs (such as &lt;code&gt;emptyDir&lt;/code&gt; with &lt;code&gt;medium: Memory&lt;/code&gt;) is not reclaimed on nodes without swap, so accumulation leads to OOM kills. &lt;strong&gt;&lt;code&gt;shmem&lt;/code&gt; sits on the anon or unevictable LRU, so it does not appear in &lt;code&gt;inactive_file&lt;/code&gt; and is not subtracted from the working set&lt;/strong&gt;. That means the increase pushes the working set up as is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For containers with no memory limit, page cache accumulates with the whole node's memory as the ceiling. Set a memory limit on containers that handle large files.&lt;/p&gt;
&lt;h3&gt;
  
  
  The RSS my application reports differs from the RSS metric
&lt;/h3&gt;

&lt;p&gt;They measure different things. The value called "RSS" in Go, Java, Node.js and so on is &lt;strong&gt;per-process RSS&lt;/strong&gt;, whereas &lt;code&gt;container_memory_rss&lt;/code&gt; is &lt;strong&gt;anonymous pages charged to the container's cgroup&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Backing value&lt;/th&gt;
&lt;th&gt;What it includes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;process.memoryUsage().rss&lt;/code&gt; in Node.js, the &lt;code&gt;RSS&lt;/code&gt; column of &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;top&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Language runtime or tools such as &lt;code&gt;ps&lt;/code&gt; (per process)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VmRSS&lt;/code&gt; in &lt;code&gt;/proc/[pid]/status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RssAnon&lt;/code&gt; + &lt;code&gt;RssFile&lt;/code&gt; + &lt;code&gt;RssShmem&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_rss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet; per cgroup)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anon&lt;/code&gt; in cgroup v2 &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Anonymous pages only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are two main differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whether file mappings are included&lt;/strong&gt;: a process's RSS includes the pages of the executable, shared libraries and &lt;code&gt;mmap&lt;/code&gt;ed files that are in physical memory (&lt;code&gt;RssFile&lt;/code&gt;). &lt;code&gt;container_memory_rss&lt;/code&gt; does not. So &lt;strong&gt;process RSS also includes &lt;code&gt;RssFile&lt;/code&gt; and &lt;code&gt;RssShmem&lt;/code&gt;&lt;/strong&gt;. Shared-page charging and sampling mean the difference is not necessarily an exact subtraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit of aggregation&lt;/strong&gt;: a process's RSS is for one process. &lt;code&gt;container_memory_rss&lt;/code&gt; is per container cgroup, so it is the total of all processes in the container. Also, a process's RSS counts shared pages in each process, whereas in a cgroup a page is charged to only one cgroup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that JVM &lt;code&gt;Runtime.totalMemory()&lt;/code&gt; and JMX heap usage are not RSS to begin with; they are the heap size. Metaspace, code cache, thread stacks and GC bookkeeping areas are not included, so you cannot compare these values directly with the memory limit.&lt;/p&gt;

&lt;p&gt;Neither value is used in the OOM kill decision. What memcg OOM compares against the limit is &lt;code&gt;memory.current&lt;/code&gt; (&lt;code&gt;container_memory_usage_bytes&lt;/code&gt;). Global OOM follows a node-side allocation failure after reclaim, potentially within allocation constraints; it has no per-container usage threshold.&lt;/p&gt;
&lt;h3&gt;
  
  
  Which metric should I use for node memory utilization
&lt;/h3&gt;

&lt;p&gt;Use them according to purpose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Metric to look at&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Get a rough sense of the node's headroom&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt; / &lt;code&gt;node_memory_MemTotal_bytes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;node-exporter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge whether eviction will occur&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;container_memory_working_set_bytes{id="/"}&lt;/code&gt; / &lt;code&gt;kube_node_status_capacity{resource="memory"}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;cAdvisor (embedded in kubelet) / kube-state-metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge whether a Pod can be placed&lt;/td&gt;
&lt;td&gt;The sum of &lt;code&gt;kube_pod_resource_request{resource="memory"}&lt;/code&gt; (the sum of requests, not usage)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;kube-scheduler&lt;/strong&gt; &lt;code&gt;/metrics/resources&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; What emits &lt;code&gt;kube_pod_resource_request&lt;/code&gt; is &lt;strong&gt;kube-scheduler&lt;/strong&gt;, not kube-state-metrics (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/scheduler/metrics/resources/resources.go#L54-L71" rel="noopener noreferrer"&gt;resources.go&lt;/a&gt;). kube-state-metrics also has a similarly named &lt;code&gt;kube_pod_container_resource_requests&lt;/code&gt;, but kube-state-metrics itself says in its help text that it recommends using the more accurate kube-scheduler metrics. Unless Prometheus is configured to scrape kube-scheduler's &lt;code&gt;/metrics/resources&lt;/code&gt;, &lt;code&gt;kube_pod_resource_request&lt;/code&gt; is not collected.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Version and configuration differences
&lt;/h2&gt;

&lt;p&gt;Statistics collection and Memory QoS behavior depend on the Kubernetes version and kubelet configuration.&lt;/p&gt;
&lt;h3&gt;
  
  
  cgroup v2 prerequisites
&lt;/h3&gt;

&lt;p&gt;Kubernetes support for cgroup v2 became stable in v1.25. The documented requirements include Linux 5.8 or later, a compatible runtime and matching systemd cgroup drivers. Confirm that the following command prints &lt;code&gt;cgroup2fs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-fc&lt;/span&gt; %T /sys/fs/cgroup/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;See &lt;a href="https://kubernetes.io/docs/concepts/architecture/cgroups/" rel="noopener noreferrer"&gt;About cgroup v2&lt;/a&gt;. The official blog, &lt;a href="https://kubernetes.io/blog/2026/10/06/kubernetes-cgroups-v2-shift/" rel="noopener noreferrer"&gt;The Shift to cgroup v2 in Kubernetes&lt;/a&gt;, also explains active page cache and group OOM handling. Its Memory QoS discussion uses v1.36 settings; the v1.37 defaults below differ.&lt;/p&gt;
&lt;h3&gt;
  
  
  KEP-2371: cAdvisor-less, CRI-full Container and Pod Stats
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2371-cri-pod-container-stats" rel="noopener noreferrer"&gt;KEP-2371&lt;/a&gt; moves the source of container and node metrics from the kubelet's embedded cAdvisor to the container runtime (CRI). Its goal is to remove the situation where the kubelet collects duplicate statistics from both cAdvisor and CRI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;/metrics/cadvisor&lt;/code&gt; endpoint and the &lt;code&gt;container_*&lt;/code&gt; metric names are kept, and the plan is to change only where the values come from to CRI&lt;/li&gt;
&lt;li&gt;The feature gate &lt;code&gt;PodAndContainerStatsFromCRI&lt;/code&gt; was Alpha in v1.23 and becomes Beta in v1.37, but &lt;strong&gt;even at Beta it is disabled by default&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the definition of the working set and the eviction decision method described in this article will not change for the time being. If this migration becomes the default in the future, the source of values becomes the runtime side, so small differences may arise from differences in how cgroups are read.&lt;/p&gt;
&lt;h3&gt;
  
  
  KEP-2570: Support Memory QoS with cgroups v2
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2570-memory-qos" rel="noopener noreferrer"&gt;KEP-2570&lt;/a&gt; reflects &lt;code&gt;requests.memory&lt;/code&gt; into cgroup v2 memory protection settings. When enabled, the memory for the request is protected from reclaim, and throttling kicks in before the limit is reached.&lt;/p&gt;

&lt;p&gt;The feature gate &lt;code&gt;MemoryQoS&lt;/code&gt; has been Alpha (disabled by default) since v1.22 and &lt;strong&gt;became Beta and enabled by default in v1.37&lt;/strong&gt;. However, &lt;strong&gt;just enabling the feature gate does not make &lt;code&gt;requests.memory&lt;/code&gt; reflected in the cgroup&lt;/strong&gt;. To reflect it, the kubelet needs the following settings, both disabled by default as of v1.37.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memoryReservationPolicy: TieredReservation&lt;/code&gt;: sets &lt;code&gt;requests.memory&lt;/code&gt; into &lt;code&gt;memory.min&lt;/code&gt; for Guaranteed and &lt;code&gt;memory.low&lt;/code&gt; for the rest. A setting added to KubeletConfiguration in v1.36, with a default of &lt;code&gt;None&lt;/code&gt;. With &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;memory.min&lt;/code&gt; is not set&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memoryThrottlingFactor&lt;/code&gt;: sets &lt;code&gt;memory.high&lt;/code&gt; to &lt;code&gt;requests.memory + factor × (limits.memory - requests.memory)&lt;/code&gt;. Up to v1.36 the default was &lt;code&gt;0.9&lt;/code&gt;, but the feature gate itself was disabled by default then, so it had no actual effect. In v1.37 the default is &lt;code&gt;nil&lt;/code&gt;, and unless you set it explicitly &lt;code&gt;memory.high&lt;/code&gt; is not set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both can be confirmed in the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L899-L917" rel="noopener noreferrer"&gt;KubeletConfiguration type definition&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="c"&gt;// MemoryThrottlingFactor specifies the factor multiplied by the memory limit or node allocatable memory&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="c"&gt;// Default: nil&lt;/span&gt;
    &lt;span class="n"&gt;MemoryThrottlingFactor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`json:"memoryThrottlingFactor,omitempty"`&lt;/span&gt;
    &lt;span class="c"&gt;// MemoryReservationPolicy controls how the kubelet applies cgroup v2 memory protection.&lt;/span&gt;
    &lt;span class="c"&gt;// "None" (default): The kubelet does not set memory.min for containers and pods,&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="n"&gt;MemoryReservationPolicy&lt;/span&gt; &lt;span class="n"&gt;MemoryReservationPolicy&lt;/span&gt; &lt;span class="s"&gt;`json:"memoryReservationPolicy,omitempty"`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So even on a v1.37 cluster, &lt;code&gt;requests.memory&lt;/code&gt; is not reflected in the cgroup, as in the table above. &lt;strong&gt;"Not reflected" is only about the default configuration&lt;/strong&gt;, and it is reflected if you set the two above.&lt;/p&gt;

&lt;p&gt;What is actually written with the default configuration is as in the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L207-L218" rel="noopener noreferrer"&gt;implementation&lt;/a&gt;. When &lt;code&gt;memoryReservationPolicy&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;, &lt;strong&gt;an explicit &lt;code&gt;0&lt;/code&gt; is written&lt;/strong&gt; instead of &lt;code&gt;requests.memory&lt;/code&gt; (equivalent to no protection).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;memoryRequest&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memoryReservationPolicy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;kubeletconfiginternal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TieredReservationMemoryReservationPolicy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;...&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;unified&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cgroup2MemoryMin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;
            &lt;span class="n"&gt;unified&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cgroup2MemoryLow&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So with the v1.37 default configuration, a container's cgroup has &lt;code&gt;memory.min = 0&lt;/code&gt;, &lt;code&gt;memory.low = 0&lt;/code&gt; and &lt;code&gt;memory.high&lt;/code&gt; unset (&lt;code&gt;max&lt;/code&gt;), and only &lt;code&gt;memory.max&lt;/code&gt; (= &lt;code&gt;limits.memory&lt;/code&gt;) is in effect. If you use a managed service, check the provider's release notes to see whether these settings get enabled.&lt;/p&gt;
&lt;h4&gt;
  
  
  What the cgroup v2 values become if you enable it
&lt;/h4&gt;

&lt;p&gt;This section works out what gets written to each cgroup v2 file &lt;strong&gt;when you explicitly set &lt;code&gt;memoryThrottlingFactor: 0.9&lt;/code&gt;, the default up to v1.36, and also enable &lt;code&gt;memoryReservationPolicy: TieredReservation&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  kubelet configuration example and the values written for each QoS class
  &lt;p&gt;The kubelet configuration looks like this. &lt;code&gt;MemoryQoS&lt;/code&gt; is enabled by default in v1.37, so you can omit &lt;code&gt;featureGates&lt;/code&gt;, but I include it to make the intent explicit.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /var/lib/kubelet/config.yaml (KubeletConfiguration)&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubelet.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KubeletConfiguration&lt;/span&gt;
&lt;span class="na"&gt;featureGates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;MemoryQoS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;            &lt;span class="c1"&gt;# true by default in v1.37&lt;/span&gt;
&lt;span class="na"&gt;memoryReservationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TieredReservation&lt;/span&gt;   &lt;span class="c1"&gt;# default is None&lt;/span&gt;
&lt;span class="na"&gt;memoryThrottlingFactor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9&lt;/span&gt;                  &lt;span class="c1"&gt;# default in v1.37 is nil (= memory.high is not set)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Suppose you create the following Pod in this state. Assume the node's Allocatable memory is 16Gi.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;requests.memory&lt;/code&gt; and &lt;code&gt;limits.memory&lt;/code&gt; differ, so this Pod's QoS class is Burstable. The following values are written to the container's cgroup.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Calculation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1073741824&lt;/code&gt; (1Gi)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;limits.memory&lt;/code&gt; as is (set as before, independent of MemoryQoS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;268435456&lt;/code&gt; (256Mi)&lt;/td&gt;
&lt;td&gt;Burstable, so &lt;code&gt;requests.memory&lt;/code&gt; goes to &lt;code&gt;memory.low&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.min&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requests.memory&lt;/code&gt; goes in only for Guaranteed. Otherwise an explicit &lt;code&gt;0&lt;/code&gt; is written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;993210368&lt;/code&gt; (about 947 MiB)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;floor((256Mi + (1Gi - 256Mi) × 0.9) / 4096) × 4096&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;memory.high&lt;/code&gt; calculation is rounded down to a multiple of the page size (4 KiB), as in the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L238-L258" rel="noopener noreferrer"&gt;implementation&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;            &lt;span class="n"&gt;memoryHigh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryLimitVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryRequest&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memoryThrottlingFactor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultPageSize&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;defaultPageSize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this container is &lt;strong&gt;subject to strong reclaim pressure and throttling at about 947 MiB, before the 1Gi hard limit&lt;/strong&gt;. Up to 256Mi it is also less likely to be reclaimed thanks to &lt;code&gt;memory.low&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Organized by QoS class, it looks like this. What matters is that even with the same &lt;code&gt;requests&lt;/code&gt; / &lt;code&gt;limits&lt;/code&gt;, the files that get set change with the QoS class.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;QoS class&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.min&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.low&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.high&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Guaranteed (&lt;code&gt;requests&lt;/code&gt; = &lt;code&gt;limits&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requests.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not set&lt;/strong&gt; (stays &lt;code&gt;max&lt;/code&gt;; excluded when &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;limits&lt;/code&gt; are equal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burstable (&lt;code&gt;requests&lt;/code&gt; &amp;lt; &lt;code&gt;limits&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requests.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;floor((req + (lim − req) × factor) / page size) × page size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burstable (no &lt;code&gt;limits&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;requests.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same formula using &lt;strong&gt;the node's Allocatable&lt;/strong&gt; instead of &lt;code&gt;limits&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BestEffort (no &lt;code&gt;requests&lt;/code&gt;/&lt;code&gt;limits&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;floor(Allocatable × factor / page size) × page size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a BestEffort Pod on the same node as the example above (Allocatable 16Gi), &lt;code&gt;memory.high&lt;/code&gt; is &lt;code&gt;floor(16Gi × 0.9 / 4096) × 4096 = 15461879808&lt;/code&gt; (about 14.4 GiB). &lt;strong&gt;&lt;code&gt;memory.high&lt;/code&gt; is also set on containers with no limit&lt;/strong&gt;, so enabling &lt;code&gt;memoryThrottlingFactor&lt;/code&gt; changes how page cache accumulates as well.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;memory.min&lt;/code&gt; and &lt;code&gt;memory.low&lt;/code&gt; are set not only on the container cgroup but also on higher levels (&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/cm/qos_container_manager_linux.go#L332-L369" rel="noopener noreferrer"&gt;qos_container_manager_linux.go&lt;/a&gt;). The kernel evaluates protection by walking up through ancestor cgroups, so the same protection is needed above as well.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.min&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.low&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/kubepods.slice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sum of Guaranteed requests &lt;strong&gt;+&lt;/strong&gt; sum of Burstable requests&lt;/td&gt;
&lt;td&gt;Sum of Burstable requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/kubepods.slice/kubepods-burstable.slice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sum of Burstable requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod cgroup&lt;/td&gt;
&lt;td&gt;The Pod's request if Guaranteed&lt;/td&gt;
&lt;td&gt;The Pod's request if Burstable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container cgroup&lt;/td&gt;
&lt;td&gt;As in the table above&lt;/td&gt;
&lt;td&gt;As in the table above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can check how much is protected across the whole node with the following metrics the kubelet exposes (both ALPHA as of v1.37).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubelet_memory_qos_node_memory_min_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;Configured &lt;code&gt;memory.min&lt;/code&gt; protection budget for Guaranteed Pods, not actual resident memory or the effective protected amount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubelet_memory_qos_node_memory_low_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;Configured &lt;code&gt;memory.low&lt;/code&gt; protection budget for Burstable Pods&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; With &lt;code&gt;memoryReservationPolicy: TieredReservation&lt;/code&gt;, Guaranteed requests become &lt;code&gt;memory.min&lt;/code&gt; protection budgets. This does not preallocate physical memory. The kernel protects the used portion within the effective boundary determined by ancestors and competing protection. Excessive protection can leave too little reclaimable memory elsewhere and destabilize the node. Right-size requests before enabling it.&lt;/p&gt;

&lt;p&gt;If you are considering enabling it, the precondition is to first right-size requests with Align memory request with actual usage.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on verification with kind
&lt;/h2&gt;

&lt;p&gt;The following examples compare protection settings, page cache and anonymous memory in a local kind cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying it with kind
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;
  Steps to bring up a local v1.37 cluster with kind and verify
  &lt;p&gt;These configuration examples and recorded values use kind with Kubernetes v1.37.0. The implementation references elsewhere target v1.37.1. The following configuration enables Memory QoS protection and throttling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# kind-memqos.yaml&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kind.x-k8s.io/v1alpha4&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memqos&lt;/span&gt;
&lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;control-plane&lt;/span&gt;
  &lt;span class="c1"&gt;# the kind default image changes with the version, so pin it explicitly&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5&lt;/span&gt;
  &lt;span class="na"&gt;kubeadmConfigPatches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;kind: KubeletConfiguration&lt;/span&gt;
    &lt;span class="s"&gt;featureGates:&lt;/span&gt;
      &lt;span class="s"&gt;MemoryQoS: true&lt;/span&gt;
    &lt;span class="s"&gt;memoryReservationPolicy: TieredReservation&lt;/span&gt;
    &lt;span class="s"&gt;memoryThrottlingFactor: 0.9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you omit &lt;code&gt;image&lt;/code&gt;, the default image embedded in kind is used, but that changes with each kind version. The digest above is the same as the default of kind v0.33.0; to try a different Kubernetes version, replace it with the digest listed in the &lt;a href="https://github.com/kubernetes-sigs/kind/releases" rel="noopener noreferrer"&gt;kind release notes&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--config&lt;/span&gt; kind-memqos.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check that the settings reached the kubelet&lt;/span&gt;
kubectl get &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="s2"&gt;"/api/v1/nodes/memqos-control-plane/proxy/configz"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.kubeletconfig | {memoryThrottlingFactor, memoryReservationPolicy, featureGates}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memoryThrottlingFactor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memoryReservationPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TieredReservation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"featureGates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MemoryQoS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You read cgroup files by going into the node container. &lt;strong&gt;kind puts the cgroup root at &lt;code&gt;/kubelet.slice/kubelet-kubepods.slice&lt;/code&gt;&lt;/strong&gt;, so note that the path differs from &lt;code&gt;/kubepods.slice&lt;/code&gt; in a real cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: Two behaviors differ from a host running Kubernetes directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One is the root cgroup fallback described earlier. A kind node is a container and has its own cgroup namespace, so the &lt;code&gt;/sys/fs/cgroup&lt;/code&gt; visible from inside the node is actually a delegated &lt;strong&gt;non-root&lt;/strong&gt; cgroup. As a result &lt;code&gt;memory.current&lt;/code&gt; exists, and the &lt;code&gt;anon + file&lt;/code&gt; substitution does not happen. The difference from a real machine is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inside a kind node (root of the cgroup namespace = actually a non-root cgroup)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;memqos-control-plane &lt;span class="nb"&gt;ls&lt;/span&gt; /sys/fs/cgroup/memory.current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sys/fs/cgroup/memory.current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the real host (the root cgroup of cgroup v2)&lt;/span&gt;
colima ssh &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /sys/fs/cgroup/memory.current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls: /sys/fs/cgroup/memory.current: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;colima ssh &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /sys/fs/cgroup/memory.stat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sys/fs/cgroup/memory.stat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other is eviction. kind overrides &lt;code&gt;evictionHard&lt;/code&gt; with disk-related thresholds and omits &lt;code&gt;memory.available&lt;/code&gt;. In this configuration there is no memory hard-eviction threshold; without other reservations, memory Capacity and Allocatable match. Soft thresholds and custom settings can change this. Set &lt;code&gt;evictionHard&lt;/code&gt; explicitly to test memory eviction.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;sample-burstable.yaml&lt;/code&gt; and wait for the application container before inspecting its cgroup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sample-burstable&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:1.27&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; sample-burstable.yaml
kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready pod/sample-burstable &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Identify the cgroup by the real container ID (a glob can pick up the pause side, as explained below)&lt;/span&gt;
&lt;span class="nv"&gt;CID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod sample-burstable &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.containerStatuses[0].containerID}'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|containerd://||'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;memqos-control-plane sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    d=&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;(find /sys/fs/cgroup/kubelet.slice -type d -name 'cri-containerd-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.scope')
    grep . &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;d/memory.max &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;d/memory.min &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;d/memory.low &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;d/memory.high"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sys/fs/cgroup/.../cri-containerd-50fc89e1....scope/memory.max:1073741824
/sys/fs/cgroup/.../cri-containerd-50fc89e1....scope/memory.min:0
/sys/fs/cgroup/.../cri-containerd-50fc89e1....scope/memory.low:268435456
/sys/fs/cgroup/.../cri-containerd-50fc89e1....scope/memory.high:993210368
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are measured values on a node with Allocatable of 2005512Ki (= 2053644288 bytes) with &lt;code&gt;memoryThrottlingFactor: 0.9&lt;/code&gt; set.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pod&lt;/th&gt;
&lt;th&gt;QoS&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.min&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.low&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;memory.high&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;requests: 256Mi&lt;/code&gt; / &lt;code&gt;limits: 1Gi&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Burstable&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;268435456&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;993210368&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;requests: 128Mi&lt;/code&gt; / no limits&lt;/td&gt;
&lt;td&gt;Burstable&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;134217728&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1861701632&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;memory only &lt;code&gt;requests&lt;/code&gt; = &lt;code&gt;limits&lt;/code&gt; = 512Mi&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Burstable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;536870912&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;max&lt;/strong&gt; (not set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpu and memory &lt;code&gt;requests&lt;/code&gt; = &lt;code&gt;limits&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Guaranteed&lt;/td&gt;
&lt;td&gt;536870912&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;max&lt;/strong&gt; (not set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nothing specified&lt;/td&gt;
&lt;td&gt;BestEffort&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1848279040&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each &lt;code&gt;memory.high&lt;/code&gt; follows the formula &lt;code&gt;floor((req + (lim − req) × factor) / page size) × page size&lt;/code&gt; described earlier. For Pods with no &lt;code&gt;limits&lt;/code&gt;, the node's Allocatable is used for &lt;code&gt;lim&lt;/code&gt;, and for BestEffort &lt;code&gt;req&lt;/code&gt; is 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning: Setting only memory to &lt;code&gt;requests&lt;/code&gt; = &lt;code&gt;limits&lt;/code&gt; does not make a Pod Guaranteed.&lt;/strong&gt; Guaranteed requires &lt;strong&gt;both cpu and memory&lt;/strong&gt; to have &lt;code&gt;requests&lt;/code&gt; = &lt;code&gt;limits&lt;/code&gt; in every container. A Pod with only memory matched is Burstable, so &lt;code&gt;requests.memory&lt;/code&gt; goes to &lt;code&gt;memory.low&lt;/code&gt;, not &lt;code&gt;memory.min&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;memory.high&lt;/code&gt; stays unset (&lt;code&gt;max&lt;/code&gt;) not because of the QoS class but on the condition &lt;code&gt;requests.memory == limits.memory&lt;/code&gt; (the &lt;code&gt;memoryRequest != memoryLimitSpec&lt;/code&gt; check in the &lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L226" rel="noopener noreferrer"&gt;implementation&lt;/a&gt;), so even a Burstable Pod with only memory matched stays at &lt;code&gt;max&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                    memory.min   memory.low   memory.high
Burstable with only memory matched           0    536870912           max
Guaranteed with cpu and memory matched  536870912          0           max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, besides the real container, a Pod has a pause (sandbox) container's &lt;code&gt;cri-containerd-*.scope&lt;/code&gt; alongside it, so a glob for &lt;code&gt;cri-containerd-*.scope&lt;/code&gt; &lt;strong&gt;can match both pause and application containers even in a single-container Pod&lt;/strong&gt;. Which comes first depends on the container ID order, so using the first glob result may read the pause side, and &lt;code&gt;memory.low&lt;/code&gt; and &lt;code&gt;memory.high&lt;/code&gt; can look like &lt;code&gt;0&lt;/code&gt; / &lt;code&gt;max&lt;/code&gt;. Identify it with &lt;code&gt;.status.containerStatuses[].containerID&lt;/code&gt; as in the command above.&lt;/p&gt;

&lt;p&gt;Protection is also set on higher levels, not only on the container cgroup. On &lt;code&gt;kubelet-kubepods.slice&lt;/code&gt; (equivalent to &lt;code&gt;/kubepods.slice&lt;/code&gt; in a real cluster), &lt;code&gt;memory.min&lt;/code&gt; is the sum of Guaranteed and Burstable requests and &lt;code&gt;memory.low&lt;/code&gt; is the sum of Burstable requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubelet-kubepods.slice            memory.min 1243611136   memory.low 706740224
kubelet-kubepods-burstable.slice  memory.min 0            memory.low 706740224
kubelet-kubepods-besteffort.slice memory.min 0            memory.low 0

1243611136 - 706740224 = 536870912 = 512Mi  ← matches the Guaranteed Pod's request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kubelet metrics described earlier can also be obtained from this endpoint. &lt;code&gt;memory_min_bytes&lt;/code&gt; covers only the Guaranteed part, so note that its value differs from the parent Pod cgroup's &lt;code&gt;memory.min&lt;/code&gt; (Guaranteed + Burstable).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="s2"&gt;"/api/v1/nodes/memqos-control-plane/proxy/metrics"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep &lt;/span&gt;kubelet_memory_qos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubelet_memory_qos_node_memory_low_bytes 7.06740224e+08
kubelet_memory_qos_node_memory_min_bytes 5.36870912e+08
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing page cache with anonymous memory
&lt;/h3&gt;

&lt;p&gt;&lt;/p&gt;
  Comparing reclaimable page cache with an anonymous-memory OOM
  &lt;p&gt;To compare reclaimable cache with anonymous memory, write six times the container's 256Mi limit to a disk-backed &lt;code&gt;emptyDir&lt;/code&gt;. This example uses the Memory QoS configuration above, including &lt;code&gt;memory.high&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;memtest.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memtest&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;busybox:1.37&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;64Mi&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disk&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/disk&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disk&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; memtest.yaml
kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready pod/memtest &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;memtest &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'dd if=/dev/zero of=/disk/big bs=1M count=1536'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1610612736 bytes (1.5GB) copied, 1.501464 seconds, 1023.0MB/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at the container's cgroup, usage is pinned just short of the limit, but &lt;strong&gt;no OOM occurred even once&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memory.max      268435456   (256Mi)
memory.high     248299520   ← floor((64Mi + (256Mi - 64Mi) * 0.9) / 4096) * 4096
memory.current  246484992
memory.peak     249049088

--- memory.events ---
low 0
high 2666        ← number of times it was throttled for exceeding memory.high
max 0            ← memory.max was not reached
oom 0
oom_kill 0       ← no OOM kill occurred
oom_group_kill 0

--- memory.stat (excerpt) ---
file           235982848
inactive_file  235937792   ← nearly all of it is reclaimable inactive page cache
active_file        45056
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point &lt;strong&gt;the working set is &lt;code&gt;246484992 - 235937792 = 10547200&lt;/code&gt; (about 10 MiB)&lt;/strong&gt;. Here reclaimable page cache dominates usage while the working set stays small. Because &lt;code&gt;high&lt;/code&gt; increased and &lt;code&gt;max&lt;/code&gt; remained zero, this record demonstrates reclaim and throttling at &lt;code&gt;memory.high&lt;/code&gt;; it does not directly test reclaim at &lt;code&gt;memory.max&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the other hand, doing the same with anonymous pages leads to an OOM kill. &lt;code&gt;tail /dev/zero&lt;/code&gt; keeps allocating memory until it finishes reading its input, and on a node without swap the retained anonymous pages cannot normally be reclaimed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run oomtest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.37 &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--overrides&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"spec":{"containers":[{"name":"app","image":"busybox:1.37",
      "command":["sh","-c","tail /dev/zero"],
      "resources":{"limits":{"memory":"64Mi"}}}]}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod oomtest &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.containerStatuses[0].state.terminated}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"exitCode":137,"reason":"OOMKilled",...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel log has a line containing &lt;code&gt;oom_memcg=&lt;/code&gt;. As described earlier, the kubelet's parser matches this format, so the cgroup can be extracted, and as a result no &lt;code&gt;SystemOOM&lt;/code&gt; event is recorded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-9be8306a....scope,
mems_allowed=0,oom_memcg=/docker/.../kubelet-kubepods-burstable-pod....slice,
task_memcg=/docker/.../cri-containerd-9be8306a....scope,task=tail,pid=4867,uid=0

Memory cgroup out of memory: Killed process 4867 (tail) total-vm:68880kB,
anon-rss:64768kB, file-rss:1280kB, shmem-rss:0kB, UID:0 pgtables:176kB oom_score_adj:984
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get events &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SystemOOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No resources found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that after an OOM kill &lt;strong&gt;the container's cgroup disappears, so the &lt;code&gt;container_oom_events_total&lt;/code&gt; series itself goes away&lt;/strong&gt;. It is the same even if you do not let it be recreated with &lt;code&gt;restartPolicy: Never&lt;/code&gt;. To trace the number of OOMs afterward, use &lt;code&gt;node_vmstat_oom_kill&lt;/code&gt; (per node) or &lt;code&gt;kube_pod_container_status_last_terminated_reason&lt;/code&gt;.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;When the dedicated test cluster is no longer needed, remove it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; memqos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Terminology and kernel references
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;
  Why this article distinguishes memcg OOM and global OOM
  &lt;h3&gt;
  
  
  How this article names the two OOM kill paths
&lt;/h3&gt;

&lt;p&gt;There are two paths by which a Pod's process gets killed for memory reasons, and &lt;strong&gt;they differ in both cause and blast radius&lt;/strong&gt;. If you call both "OOM" you cannot tell them apart, so &lt;strong&gt;this article uses the following two names throughout&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name in this article&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Triggering cause&lt;/th&gt;
&lt;th&gt;What gets killed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;memcg OOM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enforcing a configured limit&lt;/td&gt;
&lt;td&gt;An allocation charge fails against a container, Pod or &lt;code&gt;/kubepods.slice&lt;/code&gt; &lt;code&gt;memory.max&lt;/code&gt;, and reclaim cannot create enough headroom&lt;/td&gt;
&lt;td&gt;Only processes inside the cgroup that hit its limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;global OOM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeping the whole system alive (protecting the node)&lt;/td&gt;
&lt;td&gt;A node-side allocation cannot be satisfied, potentially within NUMA or cpuset constraints&lt;/td&gt;
&lt;td&gt;Eligible processes within the allocation constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;memcg OOM starts from a cgroup limit; global OOM starts from a node-side allocation failure.&lt;/strong&gt; When a container is below its own limit, check both ancestor cgroup limits and node-side constraints.&lt;/p&gt;

&lt;p&gt;The kernel documentation describes global OOM as a way to keep the rest of the system alive:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In order to save the rest of the system, it invokes the &lt;code&gt;OOM killer&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source: &lt;a href="https://docs.kernel.org/admin-guide/mm/concepts.html#oom-killer" rel="noopener noreferrer"&gt;Concepts overview — OOM killer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The description of &lt;code&gt;memory.max&lt;/code&gt; states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the main mechanism to limit memory usage of a cgroup&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Source: &lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files" rel="noopener noreferrer"&gt;Memory Interface Files&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;cgroup v2 separates limits (&lt;code&gt;memory.max&lt;/code&gt; / &lt;code&gt;memory.high&lt;/code&gt;) from protection (&lt;code&gt;memory.min&lt;/code&gt; / &lt;code&gt;memory.low&lt;/code&gt;). memcg OOM enforces a limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;memcg OOM can happen even when the node has plenty of free memory&lt;/strong&gt;, because the allocation cannot fit within the cgroup constraint after reclaim. Global OOM does not originate from a memcg limit; NUMA policies or cpuset restrictions can cause it even when memory remains elsewhere on the node.&lt;/p&gt;

&lt;p&gt;Kubernetes also uses a cgroup limit for the &lt;code&gt;/kubepods.slice&lt;/code&gt; hierarchy when Pod allocatable enforcement is enabled. Its &lt;code&gt;memory.max&lt;/code&gt; is set to &lt;code&gt;Capacity - kube-reserved - system-reserved&lt;/code&gt; (see below), which effectively reserves memory for non-Pod processes such as the kubelet and containerd. That is not the purpose of the kernel's memcg OOM; it is Kubernetes using the mechanism that way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note: These two names are not official Kubernetes terms.&lt;/strong&gt; Kubernetes only exposes the termination reason &lt;code&gt;OOMKilled&lt;/code&gt;, and it does not distinguish the paths (the reason is explained in Why OOMKilled cannot distinguish the paths).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both are terms the kernel itself uses, and this article adopts them as they are.&lt;/strong&gt; The two are used side by side in a kernel comment (&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/oom_kill.c#L1178-L1183" rel="noopener noreferrer"&gt;mm/oom_kill.c&lt;/a&gt;, &lt;code&gt;pagefault_out_of_memory()&lt;/code&gt;):&lt;/p&gt;


&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * The pagefault handler calls here because some allocation has failed. We have
 * to take care of the memcg OOM here because this is the only safe context without
 * any locks held but let the oom killer triggered from the allocation context care
 * about the global OOM.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The actual distinction is whether the &lt;code&gt;memcg&lt;/code&gt; field of &lt;code&gt;struct oom_control&lt;/code&gt; is NULL. If it holds a value, the victim is chosen from within that cgroup; if NULL, from eligible processes under the allocation constraints (&lt;a href="https://github.com/torvalds/linux/blob/v6.12/include/linux/oom.h#L36-L37" rel="noopener noreferrer"&gt;oom.h&lt;/a&gt;). &lt;code&gt;is_memcg_oom()&lt;/code&gt; just checks this field.&lt;/p&gt;


&lt;pre class="highlight c"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/* Memory cgroup in which oom is invoked, or NULL for global oom */&lt;/span&gt;
  &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;mem_cgroup&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;memcg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;&lt;code&gt;memcg&lt;/code&gt; stands for memory cgroup (memory control group)&lt;/strong&gt;, a short form of &lt;code&gt;struct mem_cgroup&lt;/code&gt; in the code above. Variable and function names in the source (&lt;code&gt;is_memcg_oom()&lt;/code&gt;, &lt;code&gt;try_charge_memcg()&lt;/code&gt;) use the same spelling, and the kernel documentation also writes "Memory cgroup (memcg)" (&lt;code&gt;Documentation/mm/hmm.rst&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This is how they appear in the kernel log:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name in this article&lt;/th&gt;
&lt;th&gt;How it appears in the kernel log&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;memcg OOM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;constraint=CONSTRAINT_MEMCG&lt;/code&gt;, &lt;code&gt;oom_memcg=&amp;lt;cgroup path&amp;gt;&lt;/code&gt;, &lt;code&gt;Memory cgroup out of memory: Killed process ...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;global OOM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;constraint=CONSTRAINT_NONE&lt;/code&gt; (or cpuset / memory-policy constraints), &lt;code&gt;,global_oom&lt;/code&gt;, &lt;code&gt;Out of memory: Killed process ...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prefix of the &lt;code&gt;Killed process&lt;/code&gt; line directly indicates the path (&lt;a href="https://github.com/torvalds/linux/blob/v6.12/mm/oom_kill.c#L1173-L1174" rel="noopener noreferrer"&gt;oom_kill.c&lt;/a&gt;):&lt;/p&gt;


&lt;pre class="highlight c"&gt;&lt;code&gt;      &lt;span class="n"&gt;oom_kill_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;is_memcg_oom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Out of memory"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
               &lt;span class="s"&gt;"Memory cgroup out of memory"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;Also note that &lt;strong&gt;eviction is a separate mechanism from an OOM kill&lt;/strong&gt;. It is the kubelet, not the kernel, that removes the Pod, and it is observed as &lt;code&gt;Evicted&lt;/code&gt;. It is covered below as well.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Separate the values used to display usage from those used to enforce limits and trigger eviction. That distinction narrows an OOM investigation to the relevant container, ancestor cgroup or node-side constraint. Then review requests against workload history and placement density, and review limits against the memory the workload actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Components and versions
&lt;/h3&gt;

&lt;p&gt;This article assumes cgroup v2. With cgroup v1, the mapping between metrics and &lt;code&gt;memory.stat&lt;/code&gt; fields is different.&lt;/p&gt;

&lt;p&gt;The statements in this article target the following versions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Role in this article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux kernel&lt;/td&gt;
&lt;td&gt;v6.12&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/torvalds/linux" rel="noopener noreferrer"&gt;https://github.com/torvalds/linux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;cgroup v2 memory control and the OOM killer itself; the OOM message format in kernel logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes (kubelet)&lt;/td&gt;
&lt;td&gt;v1.37.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes/kubernetes" rel="noopener noreferrer"&gt;https://github.com/kubernetes/kubernetes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Eviction decisions, &lt;code&gt;oom_score_adj&lt;/code&gt;, the &lt;code&gt;SystemOOM&lt;/code&gt; event, counting for &lt;code&gt;container_oom_events_total&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes (kube-scheduler)&lt;/td&gt;
&lt;td&gt;v1.37.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes/kubernetes" rel="noopener noreferrer"&gt;https://github.com/kubernetes/kubernetes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Exposing &lt;code&gt;kube_pod_resource_request&lt;/code&gt; / &lt;code&gt;kube_pod_resource_limit&lt;/code&gt; (&lt;code&gt;/metrics/resources&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cAdvisor&lt;/td&gt;
&lt;td&gt;v0.60.5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/google/cadvisor" rel="noopener noreferrer"&gt;https://github.com/google/cadvisor&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Embedded in the kubelet; exposes &lt;code&gt;container_*&lt;/code&gt; metrics; computes the working set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;containerd&lt;/td&gt;
&lt;td&gt;v2.3.5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/containerd/containerd" rel="noopener noreferrer"&gt;https://github.com/containerd/containerd&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Decides that a container's termination reason is &lt;code&gt;OOMKilled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node-exporter&lt;/td&gt;
&lt;td&gt;v1.12.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/prometheus/node_exporter" rel="noopener noreferrer"&gt;https://github.com/prometheus/node_exporter&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Exposes &lt;code&gt;node_memory_*&lt;/code&gt; (&lt;code&gt;/proc/meminfo&lt;/code&gt;) and &lt;code&gt;node_vmstat_oom_kill&lt;/code&gt; (&lt;code&gt;/proc/vmstat&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kube-state-metrics&lt;/td&gt;
&lt;td&gt;v2.20.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes/kube-state-metrics" rel="noopener noreferrer"&gt;https://github.com/kubernetes/kube-state-metrics&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Exposes &lt;code&gt;kube_node_status_capacity&lt;/code&gt; and &lt;code&gt;kube_pod_container_status_*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics Server&lt;/td&gt;
&lt;td&gt;v0.8.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes-sigs/metrics-server" rel="noopener noreferrer"&gt;https://github.com/kubernetes-sigs/metrics-server&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Source of the values &lt;code&gt;kubectl top&lt;/code&gt; reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical Pod Autoscaler&lt;/td&gt;
&lt;td&gt;1.7.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes/autoscaler" rel="noopener noreferrer"&gt;https://github.com/kubernetes/autoscaler&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Computing and automatically applying request/limit recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kind&lt;/td&gt;
&lt;td&gt;v0.33.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kubernetes-sigs/kind" rel="noopener noreferrer"&gt;https://github.com/kubernetes-sigs/kind&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Recorded examples use kindest/node:v1.37.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I assume containerd as the container runtime. With other runtimes such as CRI-O the meaning of cgroup v2 values is the same, but the &lt;code&gt;OOMKilled&lt;/code&gt; detection logic and cgroup path naming differ.&lt;/p&gt;

&lt;p&gt;I also assume the cluster has Prometheus, node-exporter and kube-state-metrics installed and that you can run PromQL. These are not part of Kubernetes itself, so if you do not have them, substitute &lt;code&gt;kubectl top&lt;/code&gt; or read &lt;code&gt;memory.stat&lt;/code&gt; directly on the node.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;p&gt;Listed in the order they appear in the article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html#memory" rel="noopener noreferrer"&gt;Memory Resource Controller (cgroup v2)&lt;/a&gt; (definitions of &lt;code&gt;memory.current&lt;/code&gt; / &lt;code&gt;memory.max&lt;/code&gt; / &lt;code&gt;memory.stat&lt;/code&gt; and other files)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/concepts/architecture/cgroups/" rel="noopener noreferrer"&gt;About cgroup v2&lt;/a&gt; (cgroup v2 from Kubernetes' point of view)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/" rel="noopener noreferrer"&gt;Resource Management for Pods and Containers&lt;/a&gt; (basics of requests and limits)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/" rel="noopener noreferrer"&gt;Reserve Compute Resources for System Daemons&lt;/a&gt; (calculation of Allocatable)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/" rel="noopener noreferrer"&gt;Node-pressure Eviction&lt;/a&gt; (overview of eviction: signals, thresholds, selection of target Pods)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/admin-guide/mm/concepts.html#oom-killer" rel="noopener noreferrer"&gt;Concepts overview - OOM killer&lt;/a&gt; (conditions and role of the OOM killer invocation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior" rel="noopener noreferrer"&gt;Node out of memory behavior&lt;/a&gt; (behavior when eviction is not in time)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/" rel="noopener noreferrer"&gt;Pod Quality of Service Classes&lt;/a&gt; (how QoS classes are determined)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/filesystems/proc.html" rel="noopener noreferrer"&gt;proc.rst - oom_score / oom_score_adj&lt;/a&gt; (the score used to select the process to kill)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.kernel.org/admin-guide/sysctl/vm.html" rel="noopener noreferrer"&gt;sysctl/vm.rst&lt;/a&gt; (OOM-related sysctls such as &lt;code&gt;panic_on_oom&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/reference/instrumentation/understand-psi-metrics/" rel="noopener noreferrer"&gt;Understand Pressure Stall Information (PSI) Metrics&lt;/a&gt; (detecting waits for memory allocation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes-sigs/metrics-server/blob/v0.8.1/FAQ.md" rel="noopener noreferrer"&gt;Metrics Server FAQ&lt;/a&gt; (definition and limits of the values &lt;code&gt;kubectl top&lt;/code&gt; shows)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler" rel="noopener noreferrer"&gt;Vertical Pod Autoscaler&lt;/a&gt; (recommending requests and limits from usage)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Metrics covered in this article
&lt;/h3&gt;

&lt;p&gt;Grouped by emitting component. The takeaway of this article is that &lt;strong&gt;even metrics representing the same "memory usage" have different definitions if they come from different emitters&lt;/strong&gt;, so when investigating, first check which component emits the value.&lt;/p&gt;

&lt;h4&gt;
  
  
  cAdvisor (kubelet &lt;code&gt;/metrics/cadvisor&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;cAdvisor embedded in the kubelet reads cgroups and exposes them. You do not need to run a separate cAdvisor.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Backing value in cgroup v2&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_usage_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.current&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usage including everything. &lt;strong&gt;What memcg OOM is evaluated against&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.current - inactive_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The value Kubernetes treats as usage. The basis for &lt;code&gt;kubectl top&lt;/code&gt; and eviction decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_rss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anon&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Amount of anonymous pages. Its scope differs from true RSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Page cache size (includes &lt;code&gt;shmem&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_total_active_file_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;active_file&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Active page cache. Included in the working set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_total_inactive_file_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inactive_file&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inactive page cache. Subtracted from the working set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_mapped_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file_mapped&lt;/code&gt; in &lt;code&gt;memory.stat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Amount of &lt;code&gt;mmap&lt;/code&gt;ed files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_spec_memory_limit_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The container's memory limit. Exposed as &lt;strong&gt;0&lt;/strong&gt; when &lt;code&gt;memory.max&lt;/code&gt; is &lt;code&gt;max&lt;/code&gt; (no limit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_pressure_memory_waiting_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;total&lt;/code&gt; of &lt;code&gt;some&lt;/code&gt; in &lt;code&gt;memory.pressure&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cumulative time (seconds) some processes were waiting for memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_pressure_memory_stalled_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;total&lt;/code&gt; of &lt;code&gt;full&lt;/code&gt; in &lt;code&gt;memory.pressure&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cumulative time (seconds) all processes were stalled waiting for memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole node can be selected with &lt;code&gt;id="/"&lt;/code&gt;, all Pods on the node with &lt;code&gt;id=~"/kubepods|/kubepods.slice"&lt;/code&gt;, and a single Pod with &lt;code&gt;container="", image="", pod!=""&lt;/code&gt;. Only &lt;code&gt;id="/"&lt;/code&gt; has no &lt;code&gt;memory.current&lt;/code&gt;, so its usage is substituted with &lt;code&gt;anon + file&lt;/code&gt; from &lt;code&gt;memory.stat&lt;/code&gt; (kernel memory is not included). For details see Values the kubelet uses for eviction decisions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Emitted by&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_oom_events_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;kubelet&lt;/strong&gt; (the name says &lt;code&gt;container_&lt;/code&gt; but it is not cAdvisor)&lt;/td&gt;
&lt;td&gt;Number of OOM kills parsed from &lt;code&gt;oom-kill:&lt;/code&gt; lines in the kernel log. Exposed on the same &lt;code&gt;/metrics/cadvisor&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  kubelet (&lt;code&gt;/metrics/resource&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;An endpoint that exposes only CPU, memory and swap usage per container, per Pod and per node. It is a different path from &lt;code&gt;/metrics/cadvisor&lt;/code&gt;, so to scrape it with Prometheus you need to add configuration.&lt;/p&gt;

&lt;p&gt;Metrics Server gets values from here, and the official documentation also says "The metrics-server fetches resource metrics from the kubelets" (&lt;a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/" rel="noopener noreferrer"&gt;Resource metrics pipeline&lt;/a&gt;). It is not exclusive to Metrics Server, though, and Metrics Server's own README &lt;strong&gt;directs you to scrape this endpoint directly for monitoring purposes&lt;/strong&gt; ("In such cases please collect metrics from Kubelet &lt;code&gt;/metrics/resource&lt;/code&gt; endpoint directly.").&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-container working set (the same value as the metric of the same name on &lt;code&gt;/metrics/cadvisor&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pod_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Per-Pod&lt;/strong&gt; working set. Obtained from the Pod's cgroup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Per-node&lt;/strong&gt; working set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  kubelet (&lt;code&gt;/metrics&lt;/code&gt;)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubelet_node_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A gauge whose value is always &lt;code&gt;1&lt;/code&gt; (the info-metric pattern). The node name is in the &lt;code&gt;node&lt;/code&gt; label, so you can bring &lt;code&gt;node&lt;/code&gt; into the &lt;code&gt;container_*&lt;/code&gt; metrics, which have no node label, with &lt;code&gt;on(instance) group_left(node)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubelet_memory_qos_node_memory_min_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configured &lt;code&gt;memory.min&lt;/code&gt; protection budget for Guaranteed Pods (MemoryQoS; ALPHA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubelet_memory_qos_node_memory_low_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configured &lt;code&gt;memory.low&lt;/code&gt; protection budget for Burstable Pods (when MemoryQoS is enabled. ALPHA)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  node-exporter
&lt;/h4&gt;

&lt;p&gt;Turns the contents of &lt;code&gt;/proc&lt;/code&gt; directly into metrics. It knows nothing of Kubernetes concepts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_memory_MemTotal_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total physical memory on the node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The kernel's estimate of allocatable memory. &lt;strong&gt;Counts reclaimable page cache as free&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_memory_Cached_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Page cache size (includes tmpfs / shmem)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_memory_Shmem_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tmpfs / shared memory part of the above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node_vmstat_oom_kill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/proc/vmstat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of OOM kills on the node. Includes both memcg OOM and global OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  kube-state-metrics
&lt;/h4&gt;

&lt;p&gt;Converts Kubernetes API objects (spec / status) into metrics. &lt;strong&gt;It does not handle actual resource usage.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_node_status_capacity{resource="memory"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The node's Capacity. The denominator for evaluating eviction thresholds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_node_status_allocatable{resource="memory"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The amount assignable to Pods. The upper bound the scheduler uses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_container_status_last_terminated_reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The last termination reason (&lt;code&gt;reason="OOMKilled"&lt;/code&gt; detects an OOM kill). &lt;strong&gt;No series appears for a container that has never terminated&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_container_status_restarts_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container restart count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_container_resource_requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The request written in the manifest (per container)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_container_resource_limits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The limit written in the manifest (per container)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  kube-scheduler (&lt;code&gt;/metrics/resources&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Exposed on the secure port (default 10259) at &lt;code&gt;/metrics/resources&lt;/code&gt;. It is a different endpoint from &lt;code&gt;/metrics&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_resource_request{resource="memory", unit="bytes"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The sum of requests per Pod. &lt;strong&gt;No series is emitted for a Pod whose value is 0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube_pod_resource_limit{resource="memory", unit="bytes"}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The sum of limits per Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>kubernetes</category>
      <category>linux</category>
      <category>cgroups</category>
      <category>prometheus</category>
    </item>
  </channel>
</rss>
