Linux and Bash for Developers · Processes and Resources
Out of Memory: Limits, Swapping and the OOM Killer
Understand what happens when a process runs out of memory, read the signs of the OOM killer, and set limits that fail predictably.
Scenario
A batch job loads a whole export into memory. On a busy night it gets killed with no error in its own logs; the exit code is 137. Another night the whole server becomes unresponsive for minutes before recovering.
What you see in production
- Processes exit with code 137 (128 + 9, SIGKILL) and no application error.
dmesgor the journal showsOut of memory: Killed process ....- Kubernetes reports
OOMKilled; Docker shows"OOMKilled": true. - Heavy swapping makes everything slow before the kill.
The naive code
rows = open("export.csv").read().splitlines() # the whole file in memory
totals = [process(r) for r in rows] # and a second full-size list
Reproduce it
A memory limit on one process (here prlimit --as, the address-space limit) makes allocation fail inside the process, where the app can see it:
prlimit --as=300000000 python3 - <<'EOF'
try:
blocks = []
for i in range(100):
blocks.append(bytearray(10_000_000)) # 10 MB each
except MemoryError:
print(f"MemoryError after allocating about {len(blocks) * 10} MB")
EOF
bash -c 'kill -KILL $$' ; echo "a process killed by SIGKILL exits with status $?"
Output
MemoryError after allocating about 280 MB a process killed by SIGKILL exits with status 137
Run this on your computer — see “Get Started” for setup.
Root cause
When the system (or a cgroup, as in containers) has no memory left to give, the kernel's OOM killer picks a process, usually the largest, and sends SIGKILL. The process gets no chance to log or clean up, which is why the only evidence is exit code 137 and kernel logs. Processes that read whole inputs into memory, or grow caches without bounds, are the usual cause.
The fix
Process data in streams with bounded memory, and set explicit limits so failures are predictable and contained:
python3 -c 'import sys; [print(f"{i},{i % 7}") for i in range(500_000)]' > export.csv
/usr/bin/time -f "whole file: %M KB peak" python3 -c '
rows = open("export.csv").read().splitlines()
print(sum(int(r.split(",")[1]) for r in rows))' 2>&1
/usr/bin/time -f "streaming: %M KB peak" python3 -c '
with open("export.csv") as f:
print(sum(int(line.split(",")[1]) for line in f))' 2>&1
Output
1499994 whole file: 48348 KB peak 1499994 streaming: 10392 KB peak
Run this on your computer — see “Get Started” for setup.
- Stream inputs (iterate lines, use generators, paginate queries) instead of loading everything.
- Bound caches and queues.
- Set memory limits per service:
MemoryMax=in systemd,--memoryin Docker,resources.limits.memoryin Kubernetes, so one process can't take down the host. - For JVM, Node and similar runtimes, set heap sizes relative to the container limit.
Trade-offs
- Limits that are too tight cause OOM kills under normal peaks; base them on measured usage plus headroom.
- Swap can absorb short spikes but turns memory pressure into slowness; many servers disable it or keep it small.
- Restarting on OOM hides leaks; track memory growth over time.
Detect it in production
journalctl -k | grep -i 'killed process'ordmesg -T | grep -i oom.- Exit code 137 and
OOMKilledstatus in container platforms. - Memory usage graphs per process/container, with alerts on approaching limits and on steady growth.
Key points
- The OOM killer sends SIGKILL: no logs, exit code 137.
- Stream data and bound caches to keep memory flat.
- Set per-service memory limits and watch usage against them.
Further reading: proc(5): oom_score · systemd.resource-control: MemoryMax · Kubernetes: resource management
Practice questions
Why does an OOM-killed process leave no error in its own logs?
Answer
The kernel sends SIGKILL, which can't be caught, so the process can't log or clean up; evidence is in kernel logs and exit code 137.
What does exit code 137 indicate?
Answer
The process was killed by signal 9 (SIGKILL): 128 + 9.
How do you keep a batch job's memory bounded?
Answer
Stream the input (iterate lines or pages) instead of loading it all, bound caches, and set a memory limit for the service.
Open in the app to mark lessons complete, take notes and track your progress.