PostgreSQLXI · Memory and Resource ManagementMemory
PostgreSQL's memory map
What you'll learn
- Separate shared memory from per-backend memory and name what lives in each
- Compute a defensible upper bound on a cluster's memory use
- Explain why summing RSS across backends overstates the total
- Choose the right measurement for a memory question
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’s memory divides into two regions with completely different properties, and nearly every memory misunderstanding comes from conflating them.
Shared memory
Allocated once, at startup, by the postmaster. Every backend maps the
same region. Its size is fixed until the server restarts, which is why
the settings that determine it are all postmaster context.
| Region | Setting | Default |
|---|---|---|
| Buffer pool | shared_buffers | 128 MB |
| WAL buffers | wal_buffers | 1/32 of shared_buffers, capped at 16 MB |
| Lock table | max_locks_per_transaction × (max_connections + max_prepared_transactions) | 64 × 100 |
| Predicate lock table | max_pred_locks_per_transaction × the same | 64 × 100 |
| Replication slots, background workers, statistics | various |
SELECT name, pg_size_pretty(allocated_size) AS size
FROM pg_shmem_allocations
ORDER BY allocated_size DESC
LIMIT 10;
That view answers “where did my shared memory go” exactly, and it is the right first stop when a server refuses to start after a settings change.
Per-backend memory
Allocated per process, on demand, and this is where the surprises are.
| Region | Setting | Bound |
|---|---|---|
| Sorts, hashes, materialised nodes | work_mem | Per node, per participant |
| Hash nodes specifically | work_mem × hash_mem_multiplier | Same, multiplied |
| Maintenance operations | maintenance_work_mem | Per operation |
| Temporary tables | temp_buffers | Per session |
| Catalogue and relation caches | — | Unbounded, and never returned |
Why RSS lies
$ ps -o pid,rss,vsz,comm -C postgres --sort=-rss | head -8 PID RSS VSZ COMMAND
30 146100 225468 postgres
31 144960 225204 postgres
27 142572 225204 postgres
28 134936 225204 postgres
29 114128 225204 postgres
1 31788 225072 postgres
33 11484 225204 postgres$ ps -o rss= -C postgres | awk '{s+=$1} END {printf "%.0f MB\n", s/1024}'728 MB
$ ps -o pid= -C postgres | wc -l
9Nine processes, 728 MB of RSS, on a server whose shared memory segment is 128 MB.
RSS counts every page a process has touched, and the shared segment is counted in every process that has touched it. The same 128 MB is counted up to nine times.
The honest figure is:
shared memory (once) + the sum of each backend's private memory
The caches that never shrink
A backend’s catalogue cache and relation cache grow as it touches tables, functions and types, and they are never returned to the operating system while the session lives.
For most workloads this is a few megabytes and irrelevant. It becomes relevant in three situations:
Many tables. A schema with tens of thousands of tables — a multi-tenant design with a schema per tenant is the classic — can give each long-lived connection hundreds of megabytes of cache, purely from having touched them.
Long-lived pooled connections. A pooler in session mode keeps connections for days. Each accumulates cache for every table any client has ever used through it. The cluster’s memory grows for weeks and then plateaus at a number nobody predicted.
Partitioned tables with many partitions. Each partition is a relation and each is cached.
The fix is not a setting, because there is none. It is connection recycling: most poolers can retire a server connection after a number of uses or a period of time, and doing so returns the memory.
What to take from this
- Shared memory is fixed at startup; per-backend memory is on demand and effectively unbounded.
work_memis per node per participant.max_connections × work_memunderstates the exposure.- Measured: nine backends summing to 728 MB of RSS on 128 MB of shared buffers. Summing RSS double-counts the shared segment.
- PSS from
smaps_rollupis the correct measurement, needs privilege, and is not free to read. - Catalogue caches never shrink. On many-table schemas with pooled connections, recycle connections.
huge_pages = tryfails silently.onfails loudly, which is better.
Cross-course references
- Linux for Production Sysadmins — Part XL (Memory Performance) covers what the operating system means by resident memory, and why summing it across backends double-counts the shared segment.
- Docker & Containers — Part XV (Resource controls) covers the cgroup limit that decides what “out of memory” means for a containerised cluster.
Quiz
Knowledge check · 6 questions
Q1. A monitoring system sums RSS across all postgres processes and reports 40 GB on a server with 8 GB of shared_buffers and 200 connections. What is wrong with the figure?
Q2. A capacity plan budgets memory as max_connections times work_mem. Why does this understate the requirement?
Q3. A multi-tenant cluster with 30,000 tables and a session-mode pooler shows memory growing steadily for weeks before plateauing at a level nobody predicted. What is the mechanism?
Q4. Which of these live in PostgreSQL's shared memory segment, fixed at startup? Select all that apply.
Q5. Setting huge_pages to 'try' means the server silently falls back to 4 kB pages when huge pages are unavailable, so a cluster can lose the benefit without any notice.
Q6. How would you produce a defensible figure for a cluster's total memory use, and what would you do if PSS were unavailable?
Passing score: 75%. Answers are checked in this browser.