A Kubernetes resource request and a resource limit belong to different decisions. Requests participate in scheduling and resource sharing; limits configure execution constraints supported by the runtime and operating system. Treating a limit as the scheduler’s reservation obscures both mechanisms. A useful resource policy identifies placement needs, acceptable contention and the failure behavior when a running workload reaches its boundary.
CPU and memory fail differently
On a typical Linux setup, a CPU limit becomes a bandwidth constraint: a busy group can be throttled while its processes remain alive. A memory limit uses memory accounting and reclaim, with cgroup OOM handling when the constrained allocation cannot be satisfied. The kernel does not gently slow every memory allocation in the same way that CPU bandwidth can be delayed.
Memory enforcement is reactive. Do not describe it as an application callback fired at an exact byte boundary or as a guarantee that usage can never transiently exceed a configured number. The application might receive an allocation failure, or OOM handling can terminate a process. Observe the actual termination reason, container state and available memory events before naming the cause.
The memory limit covers more than the application’s intended payload. Runtime overhead, additional processes and charged cache or other memory can consume the group’s allowance. A program with a sixty-four-MiB limit cannot assume it can safely allocate a sixty-four-MiB data structure on top of everything else. Memory-backed volumes can contribute to memory consumption too; their storage appearance does not make that cost disappear.
When it breaks
An exit code of 137 indicates a SIGKILL-style termination convention; it is not sufficient evidence of OOM by itself. Correlate it with the container’s OOMKilled reason and relevant runtime or kernel evidence. A node-pressure eviction is another mechanism, with Pod-level status and eviction decisions that should not be conflated with one container exceeding its cgroup budget.
Restart policy determines what follows termination. A repeatedly leaking application under Always policy can enter a restart cycle while the underlying defect remains. With Never policy, the terminated container stays terminated and the Pod can fail. The limit contains one resource dimension; it neither repairs the application nor guarantees that neighboring workloads experience no contention before the failure.
Defaulting also matters. If a resource has a limit but no request and no admission mechanism supplies a different default, Kubernetes copies that limit into the request. Review the admitted object instead of assuming omitted requests mean zero. Matching memory requests and limits alone is not enough to establish the conventional Guaranteed QoS requirements; CPU settings and all relevant containers matter as well.
Use explicit units such as Mi and Gi for memory. Lowercase m denotes a milli-unit, not megabytes. Base limits on measured working sets and bursts, accounting for startup, concurrency and runtime overhead. A copied ratio or fixed percentile cannot choose acceptable OOM behavior for every application.
Check it quickly
Use a disposable Linux Kubernetes cluster with cgroup v2, kubectl access and python:3.11-slim-bookworm on its nodes. Save this as memory-limit.py and run python3 memory-limit.py KUBECONFIG_PATH CONTEXT with explicit test-cluster arguments. The account needs namespace creation and deletion permissions. The recorded server was Kubernetes 1.34.11. The fixture bounds its attempted payload to 128 MiB and places a 64 MiB limit on the allocating container.
import json
import subprocess
import sys
import time
from uuid import uuid4
assert len(sys.argv) == 3, 'usage: python3 example.py DISPOSABLE_KUBECONFIG CONTEXT'
base = ['kubectl', '--kubeconfig', sys.argv[1], '--context', sys.argv[2], '--request-timeout=30s']
image = 'python:3.11-slim-bookworm'
def k(*args, payload=None, expected=0):
result = subprocess.run(base + list(args), input=json.dumps(payload) if payload is not None else None,
capture_output=True, text=True, timeout=150)
if expected is not None:
assert result.returncode == expected, result.stderr
return result
def obj(namespace, kind, name=None, selector=None):
args = ['get', kind, '-n', namespace, '-o', 'json']
if name:
args.append(name)
if selector:
args += ['-l', selector]
return json.loads(k(*args).stdout)
def apply(namespace, data):
return k('apply', '-n', namespace, '-f', '-', payload=data)
def wait_for(read, ready, seconds=120):
until = time.monotonic() + seconds
while time.monotonic() < until:
value = read()
if ready(value):
return value
time.sleep(0.5)
raise AssertionError('expected Kubernetes condition did not arrive')
def pod_ready(pod):
return any(c['type'] == 'Ready' and c['status'] == 'True'
for c in pod.get('status', {}).get('conditions', []))
def spec(script='import time; time.sleep(600)'):
return {'automountServiceAccountToken': False, 'terminationGracePeriodSeconds': 1,
'containers': [{'name': 'app', 'image': image, 'imagePullPolicy': 'IfNotPresent',
'command': ['python3', '-c', script],
'resources': {'requests': {'cpu': '10m', 'memory': '24Mi'},
'limits': {'cpu': '200m', 'memory': '96Mi'}}}]}
namespace = 'notes-memory-' + uuid4().hex[:8]
k('create', 'namespace', namespace)
try:
allocator = '''from pathlib import Path
import json,time
group=Path('/sys/fs/cgroup')
print(json.dumps({'memory_max':(group/'memory.max').read_text().strip(),
'initial_memory_current':int((group/'memory.current').read_text())}),flush=True)
while not Path('/tmp/go').exists(): time.sleep(0.1)
chunks=[]
for i in range(16):
chunks.append(bytearray(8*1024*1024))
print('allocated_chunks='+str(len(chunks)),flush=True)
time.sleep(0.05)
raise RuntimeError('bounded allocation unexpectedly escaped the expected limit')
'''
constrained = spec(allocator)
constrained['restartPolicy'] = 'Never'
constrained['containers'][0]['resources'] = {
'requests': {'cpu': '10m', 'memory': '32Mi'}, 'limits': {'cpu': '200m', 'memory': '64Mi'}}
for name, podspec in [('limited', constrained), ('neighbor', spec())]:
apply(namespace, {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': name}, 'spec': podspec})
wait_for(lambda name=name: obj(namespace, 'pod', name), pod_ready)
first_log = wait_for(lambda: k('logs', '-n', namespace, 'limited').stdout.strip(), bool)
before = json.loads(first_log)
assert before['memory_max'] == str(64 * 1024 * 1024)
k('exec', '-n', namespace, 'limited', '--', 'python3', '-c',
'from pathlib import Path; Path("/tmp/go").touch()')
failed = wait_for(lambda: obj(namespace, 'pod', 'limited'), lambda p: p.get('status', {}).get('phase') == 'Failed')
status = failed['status']['containerStatuses'][0]
terminated = status['state']['terminated']
assert terminated['reason'] == 'OOMKilled' and terminated['exitCode'] == 137
assert status['restartCount'] == 0
assert failed['status'].get('reason') != 'Evicted'
neighbor = obj(namespace, 'pod', 'neighbor')
assert pod_ready(neighbor) and neighbor['status']['containerStatuses'][0]['restartCount'] == 0
output = k('logs', '-n', namespace, 'limited').stdout.splitlines()
chunks = [int(line.split('=')[1]) for line in output if line.startswith('allocated_chunks=')]
assert chunks and max(chunks) < 16
print(json.dumps({'before': before, 'last_reported_allocated_chunks': max(chunks),
'termination_reason': terminated['reason'], 'exit_code': terminated['exitCode'],
'container_restarts_with_never_policy': 0,
'pod_was_not_evicted': True, 'neighbor_still_ready': True}))
finally:
k('delete', 'namespace', namespace, '--wait=true', '--timeout=90s')
The first log records memory.max as 67108864 bytes. After a release file appears, the original container process allocates eight-MiB chunks. The recorded run reported six completed chunks before termination. Its status shows OOMKilled and exit 137, while the Pod is Failed rather than Evicted. Never policy leaves its restart count at zero.
A separate idle test Pod remains Ready with no restart at the final observation. All assertions passed and the namespace was removed. This verifies one constrained OOM event and its lifecycle result; it does not benchmark neighbor latency or establish full node isolation. Use the failure evidence to size and diagnose the workload, rather than treating a configured limit as a complete availability guarantee.
Sources
- Kubernetes: Resource Management, requests, limits, defaulting and Linux enforcement, checked 2026-09-13.
- Kubernetes: Assign Memory Resources, memory limits and observed OOM termination, checked 2026-09-13.
- Linux: Control Group v2, memory.max, reclaim and cgroup OOM behavior, checked 2026-09-13.