Kubernetes Memory: From Physical RAM to Pods
What “increase memory” physically means—from DRAM and cloud machines to nodes, cgroups, containers, and pods.
The enterprise problem and today’s slice
Enterprise problem: A customer-facing API is restarting with OOMKilled, while a batch job is Pending with Insufficient memory; treating both as “the cluster needs more RAM” risks wasting money without restoring either workload. Whole-course context: The SWE course has established workloads, infrastructure, and operations; today turns their resource settings and runtime evidence into one end-to-end capacity model. Today’s slice: We separate physical machine memory, node allocatable memory, scheduler reservations, and Linux enforcement, then choose the smallest layer that resolves each failure. End-of-day evidence: A reviewer can trace a 32 GiB node budget, explain an OOM or Pending event, and name the exact container, pod, node, or cluster change required. Still unsolved: Workload-specific heap profiling, cloud-node pricing, and production rollout policy remain environment-dependent decisions.
Customer use cases
An ambiguous memory request can send the wrong team toward the wrong change, prolonging an outage or buying idle machines. These two use cases distinguish a workload ceiling from missing placement capacity before anyone edits a resource.
| Use case ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D43-UC-01 | Payments platform operator | Stop a memory-bound API container from restarting during a measured peak | The replacement container completes the peak below its justified limit with no new OOM event | An unsafe resize is rejected when node headroom is absent; the previous OOM event and unaffected node health remain visible |
| D43-UC-02 | Data engineering lead | Run a 20 GiB ETL pod when no current node can satisfy its request | A compatible node joins, the scheduler binds the pod, and the job starts | The pod remains Pending with Insufficient memory until compatible capacity exists; unrelated running pods remain healthy |
Actor-centred user stories
Memory changes are safe only when an accountable actor can state both the intended outcome and the evidence that would disprove it. These stories make the resize and capacity-addition decisions observable instead of relying on a generic “more memory” ticket.
| Story ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D43-US-01 | D43-UC-01 | As a payments platform operator, I want to distinguish a container-limit breach from node pressure, so that I resize only the failing workload without endangering its neighbours | The termination reason is OOMKilled, measured peak usage justifies the new request and limit, the target node has headroom, and a replay completes without another OOM event |
| D43-US-02 | D43-UC-02 | As a data engineering lead, I want a Pending ETL pod to trigger compatible node capacity, so that the job runs without weakening its justified reservation | Scheduler events show Insufficient memory, a new node exposes enough allocatable memory, the pod binds there, and existing workloads remain running |
End-to-end product flows
A resource edit crosses several owners, so losing the sequence can confuse configuration with physical capacity. The happy path changes a container ceiling only after checking node headroom; the recovery path adds a machine because no existing per-node budget can fit the pod.
| Flow ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D43-FLOW-01 | D43-UC-01 | Happy | Operator opens an OOM incident for the payments API | 1. Read the container termination reason and usage curve.<br>2. Rule out node MemoryPressure.<br>3. Profile leak versus legitimate peak.<br>4. Update request and limit.<br>5. Recreate or resize the container under policy.<br>6. Replay peak traffic. | Deployment revision, old and new resource values, node headroom, load-test run ID, zero new OOM events, and stable customer requests |
| D43-FLOW-02 | D43-UC-02 | Recovery | Data lead submits a 20 GiB ETL pod and it remains Pending | 1. Read scheduler events.<br>2. Compare the request with every node’s allocatable remainder.<br>3. Keep the justified request unchanged.<br>4. Add a compatible node through the node group.<br>5. Wait for kubelet registration.<br>6. Observe scheduling and job start. | Insufficient memory event followed by node identity, allocatable memory, pod binding, job start time, and unaffected positive-control pod |
System design derived from the flows
The same word—memory—appears in hardware, Kubernetes, and Linux with different meanings, so a flat component diagram hides the real failure boundary. The architecture below follows physical bytes upward and shows where scheduling stops and runtime enforcement begins.
| Use case ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D43-UC-01 | Deployment resource edit and incident view | Kubernetes API server, scheduler, kubelet, container runtime, Linux cgroup controller, metrics pipeline | Kubernetes API desired state plus kernel cgroup counters and pod status | OOMKilled, exit 137, cgroup OOM counter, restart count, or rejected resize because node headroom is insufficient |
| D43-UC-02 | ETL workload submission and node-group capacity policy | Kubernetes API server, scheduler, cluster autoscaler, cloud or bare-metal provisioner, kubelet | Kubernetes API pod/node status plus infrastructure provider node-group state | Pending pod with Insufficient memory, failed node provisioning, incompatible taint/affinity, or absent allocatable capacity |
Data model and ownership
Capacity decisions fail when live usage, declared reservations, and physical supply are stored as if they were one number. Each record below has a distinct owner and lifecycle, so an operator can reconstruct why a workload was placed, killed, evicted, or left Pending.
Generated-application database: Not created in this slice — Kubernetes desired state, node status, kernel counters, infrastructure state, metrics, and events already provide the durable control-plane and evidence records required by both flows.
| Record or entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| WorkloadResourceRevision | Kubernetes API store, owned by the workload controller and platform policy | workload UID plus revision | Pod-template reference to container name; no cross-plane authority transfer | Namespace, the Kubernetes isolation key for the owning team | Every container request must be schedulable under policy; limit must not disguise an unprofiled leak | Created on spec change, retained in rollout history and audit, removed with workload retention policy | D43-UC-01 |
| PodMemoryStatusAndEvent | Kubernetes API status/events plus metrics store, owned by kubelet and observability platform | Pod UID plus observation timestamp or event UID | Opaque node UID and container runtime ID | Namespace | Termination reason, restart, usage, and node condition must refer to the same observed interval | Status follows the pod; metrics expire by retention; events age out but incident evidence is retained | D43-UC-01, D43-UC-02 |
| NodeCapacitySnapshot | Kubernetes Node status, owned by kubelet and platform engineering | Node UID plus observed generation | Opaque infrastructure instance ID | None — a node is cluster-scoped rather than namespace-scoped | Allocatable equals capacity less configured system, Kubernetes, and eviction reservations | Created at node registration, refreshed by kubelet, deleted when the node is retired; audit snapshot retained | D43-UC-01, D43-UC-02 |
| NodeGroupCapacityAction | Cloud or bare-metal infrastructure state, owned by platform engineering | Scaling action ID | Opaque Kubernetes cluster and eventual node UIDs | Cluster or cloud-account identifier | Requested machine shape must be compatible with pod selectors, taints, zones, and memory request | Created on scale action, completed or failed with evidence, retained for cost/audit, removed by policy | D43-UC-02 |
The physical hierarchy: where a byte lives
An operator cannot decide whether to edit YAML or buy capacity until the location of the shortage is clear. A pod ultimately uses electrical charge stored in physical dynamic random-access memory (DRAM), but several ownership and accounting layers stand between the process and those chips.
- DRAM: Memory modules attached to a server’s CPU sockets physically hold active pages.
- Cloud host or bare-metal server: On bare metal, Linux sees installed DRAM directly. In a cloud, a hypervisor assigns a virtual machine a memory size backed by provider hardware.
- Node operating system: Linux uses some memory for its kernel, page cache, kubelet, container runtime, and system daemons.
- Kubernetes node: Kubelet reports detected
capacity.memory, then exposes a smallerallocatable.memorybudget for pods after reservations and eviction headroom. - Linux cgroup: A control group accounts for a container’s processes and enforces their memory ceiling.
- Container and pod: A container is a process boundary; a pod is the scheduling unit containing one or more containers, all on the same node.
Virtual address space does not create physical RAM. It gives a process addresses that the kernel maps to resident pages, reclaimable cache, or—only when configured—swap storage.
Requests, limits, and actual usage
Confusing a request with a limit produces opposite failures: an oversized request leaves a pod Pending, while an undersized limit kills a process that might have fit on the node. Kubernetes scheduling and Linux enforcement therefore use separate values.
| Quantity | Question it answers | Enforced or consumed by |
|---|---|---|
| Node capacity | How much memory did kubelet detect on this machine? | Kubelet reports it |
| Node allocatable | How much of capacity may pods request? | Scheduler uses it as the pod budget |
| Container request | How much allocatable memory must placement reserve? | Scheduler and autoscalers |
| Container limit | What is the maximum memory charged to this container cgroup? | Linux kernel |
| Actual usage | How many bytes are charged now? | Kernel counters and metrics pipeline |
resources:
requests:
memory: "2Gi"
limits:
memory: "3Gi"
The request does not pre-allocate a contiguous 2 GiB block of DRAM, and the process need not consume it. The limit does not add memory to the node; it permits this cgroup to compete for up to 3 GiB of the node’s real memory. A pod’s placement cost includes the requests of its containers, relevant init-container rules, and any declared pod overhead.
A 32 GiB node, counted three ways
A machine label such as “32 GiB” overstates what workloads can safely reserve, so capacity planning must show every subtraction. This example separates physical supply, Kubernetes placement budget, and live demand.
| Node accounting step | Amount |
|---|---|
| VM or bare-metal memory | 32 GiB |
| System and Kubernetes reservations | −3 GiB |
| Eviction headroom | −1 GiB |
| Node allocatable | 28 GiB |
| Requests of already scheduled pods | −22 GiB |
| Remaining schedulable requests | 6 GiB |
An 8 GiB-request pod stays Pending even if a dashboard temporarily shows 10 GiB available: the scheduler protects declared reservations and sees only 6 GiB of request budget. A 4 GiB request with a 10 GiB limit can schedule, but several such pods may collectively use more than the node can supply if they peak together. That is memory overcommit, not free capacity.
OOM kill, eviction, and a Pending pod are different failures
All three symptoms mention memory, but responding to the wrong one can move rather than solve the outage. The decisive question is whether enforcement failed inside one cgroup, node safety failed across workloads, or placement failed before the pod ever ran.
| Failure | Decision layer | Evidence | Correct first move |
|---|---|---|---|
| Container exceeds its cgroup limit | Linux runtime enforcement | OOMKilled, exit 137, restart count, cgroup OOM event | Profile leak versus legitimate peak; then justify a new request and limit |
| Node approaches unsafe available memory | Kubelet node protection | MemoryPressure, eviction event, node availability metrics | Reduce pressure, rebalance, or add node capacity; preserve system headroom |
| No node fits the pod request | Kubernetes scheduling | Pending pod and Insufficient memory event | Add a compatible node, use a larger node shape, or correct an unjustified request |
Swap moves selected memory pages to slower storage and is configuration-dependent in Kubernetes environments. It can change failure timing and latency, but it does not make disk equivalent to DRAM or remove the need for a truthful node budget.
What “increase memory” changes at each level
A safe change request must name both the object being changed and the physical consequence; otherwise several teams may each interpret “increase memory” differently. The smallest effective layer depends on whether one process, one pod, one node, or total fleet capacity is the bottleneck.
| Change | What changes under the hood | What does not happen |
|---|---|---|
| Raise a container limit | Runtime configures a higher cgroup ceiling on a resized or replacement container | No DRAM is added to the node |
| Raise a container request | Scheduler must reserve more of one node’s allocatable budget | The process does not immediately consume that amount |
| Raise pod memory | Requests/limits change for the relevant app and sidecar containers; aggregate placement cost rises | A pod still cannot span two nodes |
| Resize a node | A larger VM/server supplies more machine memory and usually more allocatable memory | Existing pod specs do not automatically become sensible |
| Add nodes | The cluster gains more separate per-node allocatable pools | Memory does not become one shared cluster-wide heap |
| Add replicas | More pods can serve throughput across nodes | One process does not receive a larger heap |
On bare metal, more physical memory may literally mean installing compatible DIMMs and rebooting the server. In a cloud node group, it usually means replacing or adding virtual machines with a larger memory-backed instance shape. In both cases kubelet must register the resulting capacity before the scheduler can use it.
Vertical, horizontal, and node scaling
Autoscalers operate on different axes, so choosing one by name rather than by bottleneck can amplify cost or preserve the original failure. Vertical scaling changes a pod’s resource envelope, horizontal scaling changes pod count, and cluster scaling changes machine count.
Vertical Pod Autoscaler (VPA) can recommend or apply new resource requests according to policy. A normal spec change often creates replacement pods; in-place resize can reduce disruption when the cluster, runtime, and workload support it, but it still cannot exceed real node capacity.
Horizontal Pod Autoscaler (HPA) adds or removes replicas from a scalable workload. It improves throughput or redundancy when work can be divided, but every new replica adds requests and usage somewhere in the cluster.
Cluster Autoscaler reacts to pods that cannot schedule by adding compatible nodes from a configured group. It fixes placement capacity, not a container that repeatedly crosses an unchanged 512 MiB limit.
Commands that identify the failed layer
Current-use charts alone cannot explain why Kubernetes scheduled or killed something, so diagnosis must combine declared state, node state, and events. These commands collect each view without mutating the cluster.
# Requests, limits, last termination state, placement, and pod events
kubectl describe pod api-7d9c -n payments
# Live container usage when Metrics Server is available
kubectl top pod api-7d9c -n payments --containers
# Capacity, allocatable, aggregate requests/limits, and MemoryPressure
kubectl describe node worker-a
kubectl top node worker-a
# Scheduling, eviction, and OOM-related event chronology
kubectl get events -n payments --sort-by=.lastTimestamp
Read the last container state for OOMKilled, the pod events for Insufficient memory, and the node conditions for MemoryPressure. Compare live usage with requests and limits, but size from a representative time window and a workload profile rather than a single sample.
A practical decision sequence
Changing capacity during an incident is safer when each branch has a falsifiable observation. This sequence starts from the visible symptom and ends at the narrowest change that can remove it.
- Pending with
Insufficient memory: keep a justified request; add a compatible node or larger node shape, or correct an inflated request. OOMKilledwhile the node is healthy: profile the process, distinguish leak from legitimate peak, then raise the container request and limit only with headroom.- Node
MemoryPressureor evictions: restore node headroom, reduce unsafe overcommit, rebalance pods, or add node capacity. - Throughput pressure without a per-process memory ceiling: test horizontal scaling before making every replica larger.
- Declared requests diverge from representative usage: use VPA recommendations as evidence, load-test the proposed envelope, and roll out gradually.
The most useful ticket replaces “increase Kubernetes memory” with a statement such as: “Raise the API container request from 1 GiB to 2 GiB and limit from 2 GiB to 3 GiB because load test run-184 peaked at 2.4 GiB without a leak; place the replacement only on nodes with at least 3 GiB headroom, and roll back on any OOM or node-pressure event.”
Key takeaways
Memory incidents repeat when teams remember values but forget ownership boundaries, so retain the hierarchy rather than a single rule. Physical DRAM belongs to a machine, Kubernetes exposes a safe per-node budget, the scheduler reserves requests, and the Linux kernel enforces container cgroups.
- A cluster is a collection of separate node memory pools, not one giant shared RAM bank.
- Capacity is detected machine memory; allocatable is the smaller pod budget after reservations and eviction headroom.
- Requests govern placement; limits govern runtime enforcement; actual usage is a third value.
- A cgroup OOM kill, a kubelet eviction, and a scheduler Pending event require different fixes.
- Raising a limit permits more use of existing node RAM; resizing or adding nodes changes underlying capacity.
- VPA changes the vertical envelope, HPA changes replica count, and Cluster Autoscaler changes node count.
Checklist
A memory change is not ready until another engineer can reproduce the diagnosis and see which boundary owns the fix. Use this checklist before approving the rollout.
- [ ] I can trace memory from physical DRAM through the host, node, cgroup, container, and pod.
- [ ] I can calculate node allocatable memory and remaining schedulable requests.
- [ ] I can explain why a request neither pre-allocates DRAM nor acts as a hard ceiling.
- [ ] I can distinguish
OOMKilled, eviction underMemoryPressure, and Pending withInsufficient memory. - [ ] I have included app containers, sidecars, init-container rules, and pod overhead in the capacity review.
- [ ] I have profiled a suspected leak before feeding it a larger limit.
- [ ] I can name whether this change resizes a container, pod, node, or cluster.
- [ ] I can justify VPA, HPA, or Cluster Autoscaler from the measured bottleneck.
- [ ] I have captured pre-change evidence, a positive control, rollout proof, and rollback conditions.