Skip to main content
RunBook Academy

PostgreSQLXI · Memory and Resource ManagementMemory

PostgreSQL's memory map

Intermediate⏱ ~30 minpsqlps

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

Not yet marked complete on this device.

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.

RegionSettingDefault
Buffer poolshared_buffers128 MB
WAL bufferswal_buffers1/32 of shared_buffers, capped at 16 MB
Lock tablemax_locks_per_transaction × (max_connections + max_prepared_transactions)64 × 100
Predicate lock tablemax_pred_locks_per_transaction × the same64 × 100
Replication slots, background workers, statisticsvarious
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.

RegionSettingBound
Sorts, hashes, materialised nodeswork_memPer node, per participant
Hash nodes specificallywork_mem × hash_mem_multiplierSame, multiplied
Maintenance operationsmaintenance_work_memPer operation
Temporary tablestemp_buffersPer session
Catalogue and relation cachesUnbounded, and never returned

Why RSS lies

Read-only / Safewhat ps reports for a cluster with 128 MB of shared buffers
$ 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
Read-only / Safethe sum, against what is actually configured
$ ps -o rss= -C postgres | awk '{s+=$1} END {printf "%.0f MB\n", s/1024}'
728 MB

$ ps -o pid= -C postgres | wc -l
9

Nine 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_mem is per node per participant. max_connections × work_mem understates 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_rollup is 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 = try fails silently. on fails 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

  1. 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?

  2. Q2. A capacity plan budgets memory as max_connections times work_mem. Why does this understate the requirement?

  3. 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?

  4. Q4. Which of these live in PostgreSQL's shared memory segment, fixed at startup? Select all that apply.

  5. 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.

  6. 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.