PostgreSQLXI · Memory and Resource ManagementMemory
Linux memory pressure and the OOM killer
What you'll learn
- Read the memory limit that actually applies to a PostgreSQL process
- Explain what happens when the OOM killer selects a PostgreSQL backend
- Configure overcommit and OOM protection appropriately for a database host
- Recognise an OOM event afterwards from the evidence it leaves
Prerequisites
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
PostgreSQL asks the operating system for memory and assumes it gets what it is given. Linux, by default, promises more than it has. The gap between those two positions is where clusters die.
Reading the limit that actually applies
$ cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/memory.current; free -m | head -2 /sys/fs/cgroup/memory.max = max
/sys/fs/cgroup/memory.high = max
/sys/fs/cgroup/memory.current = 2070110208
/sys/fs/cgroup/memory.swap.max = max
total used free shared buff/cache available
Mem: 47145 9397 7769 416 30979 37747free reports the host’s 47 GB. It does not know about cgroups, and
it never has. A capacity calculation performed inside a container using
free will size shared_buffers and work_mem for memory the process
may not be allowed to use.
The authoritative figures are in the cgroup filesystem:
# cgroup v2, which is what modern distributions use
cat /sys/fs/cgroup/memory.max # hard limit; "max" means unlimited
cat /sys/fs/cgroup/memory.high # throttling threshold
cat /sys/fs/cgroup/memory.current # current usage
cat /sys/fs/cgroup/memory.swap.max # swap allowance
# cgroup v1, still present on older systems
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
On this container memory.max is max — unlimited — which is exactly
why free’s answer looks plausible and is still the wrong place to read
it from. On a Kubernetes pod with a memory limit, the two numbers differ
by an order of magnitude and nothing warns you.
Overcommit
$ cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio0
50vm.overcommit_memory | Behaviour |
|---|---|
0 (default) | Heuristic. Allows most allocations; kills later if the promise cannot be kept |
1 | Always allow. Never refuse an allocation |
2 | Never overcommit. Refuse allocations beyond swap + overcommit_ratio% of RAM |
The PostgreSQL documentation recommends 2 for dedicated database
hosts, and the reasoning is worth understanding rather than following.
Under 0, an allocation that cannot ultimately be honoured succeeds
anyway, and the process is killed later when it touches the memory.
The failure arrives at an arbitrary moment, in an arbitrary process,
with no relationship to which process asked for too much.
Under 2, the allocation fails at the point of asking. PostgreSQL
receives an out-of-memory error, the query aborts with a clear message,
and everything else keeps running.
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=80 # with swap, tune to the host
The caution: setting 2 with overcommit_ratio too low makes
allocations fail that would have been fine, and PostgreSQL will not
start if it cannot get its shared memory. Compute the allowance
deliberately: swap + (RAM × overcommit_ratio / 100).
What the OOM killer does to PostgreSQL
When the kernel kills a process to reclaim memory it chooses by a score
derived from memory usage, adjusted by oom_score_adj.
If it selects a backend, the consequence is out of proportion to the loss of one connection:
LOG: server process (PID 12345) was terminated by signal 9: Killed
DETAIL: Failed process was running: SELECT ...
LOG: terminating any other active server processes
LOG: all server processes terminated; reinitializing
The postmaster restarts every backend. A backend killed without cleanup may have left shared memory inconsistent, and the postmaster cannot prove otherwise, so it recycles the whole cluster. One killed query becomes a full disconnection event for every session.
This is the same mechanism as kill -9 from lesson IX-06, and it is why
that instruction exists.
If it selects the postmaster, the cluster is simply gone.
Recognising it afterwards
An OOM kill leaves evidence in three places, and the PostgreSQL log alone is ambiguous.
The kernel log is definitive:
dmesg -T | grep -i -E 'killed process|out of memory|oom'
journalctl -k --since '1 hour ago' | grep -i oom
The PostgreSQL log shows the consequence:
LOG: server process (PID 12345) was terminated by signal 9: Killed
signal 9 with no operator action is the signature. A
pg_terminate_backend produces signal 15 and a clean FATAL
message instead.
The cgroup counters, on a containerised deployment:
grep oom /sys/fs/cgroup/memory.events
# oom 3
# oom_kill 3
Non-zero and increasing means the container has been hitting its limit, whether or not anyone noticed.
What to take from this
freeinside a container reports the host. Readmemory.maxfrom the cgroup filesystem.memory.highthrottles rather than kills, and presents as inexplicable slowness.vm.overcommit_memory=2turns a later arbitrary kill into an immediate honest allocation failure.- An OOM kill of one backend restarts every backend on the cluster.
- Protect the postmaster with
OOMScoreAdjust, and let PostgreSQL re-raise the score for its children. - Small swap with
vm.swappiness=1. Not zero, not large. dmesgis definitive;signal 9in the PostgreSQL log is the signature.
Cross-course references
- Linux for Production Sysadmins — Part XL (Memory Performance) covers overcommit, the OOM score, and reading the kernel’s own record of the kill, which is the primary evidence for this failure.
- Docker & Containers — Part XV (Resource controls) covers the cgroup limit that triggers the kill inside a container, where the host has memory to spare and the container does not.
- Kubernetes for Production Sysadmins — Part XXXII (Node pressure and eviction) and Part XIII (QoS classes) cover which pod is chosen first, which is a decision made outside PostgreSQL entirely.
Quiz
Knowledge check · 6 questions
Q1. A PostgreSQL pod is repeatedly killed despite shared_buffers and work_mem being sized for the 64 GB that free reports inside the container. What was misread?
Q2. A containerised PostgreSQL becomes extremely slow with no lock contention and no storage latency. memory.current is close to memory.high but well below memory.max. What is happening?
Q3. Why does PostgreSQL restart every backend when the OOM killer terminates just one of them?
Q4. Which are appropriate configuration choices for a dedicated PostgreSQL host? Select all that apply.
Q5. A PostgreSQL log entry reading 'was terminated by signal 9: Killed' with no operator action is the signature of an OOM kill, confirmable in dmesg.
Q6. You are asked to size shared_buffers and work_mem for a PostgreSQL pod in Kubernetes. What would you read, and in what order?
Passing score: 75%. Answers are checked in this browser.