Chapter 02 · Cluster Architecture, Processes, Memory, Files, and Configuration

Shared Buffers, WAL Buffers, Work Memory, Maintenance Memory, and Memory Budgeting

Build a defensible PostgreSQL memory budget by separating shared allocations from per-backend and per-operation memory, then observe work_mem spills instead of tuning from folklore.

Intermediate → Advanced105–135 minutesMemory accounting + spill evidence labCurrent patched PostgreSQL 18.xNo universal tuning percentagesLast reviewed: August 2026

Learning outcomes

Memory tuning is where PostgreSQL folklore can become dangerous. Advice such as “set shared_buffers to X% of RAM” or “raise work_mem until sorts are fast” hides the real mechanism: PostgreSQL uses both shared instance memory and memory that can be allocated repeatedly by many backend processes and many plan operations. A setting that looks small in isolation can become large when multiplied by concurrency.

01

Separate shared-memory allocations such as shared_buffers/wal_buffers from per-session/per-operation memory such as work_mem.

02

Explain why one query can use multiple work_mem-bounded operations and why hash operations can use a multiplier.

03

Understand maintenance_work_mem and autovacuum memory as a different concurrency domain.

04

Use EXPLAIN (ANALYZE, BUFFERS), temporary-file evidence, and settings metadata to observe memory pressure/spilling without inventing benchmark claims.

05

Build a conservative capacity worksheet instead of copying universal percentages.

1. A memory budget has layers

At minimum, think in four layers: PostgreSQL shared memory, backend/session baseline memory, query-operation memory, and maintenance/background memory. The operating system, filesystem cache, monitoring agents, connection pooler, and other applications also need RAM. Therefore “RAM minus shared_buffers equals free for work_mem” is not a valid budget.

Memory class Examples Concurrency characteristic
Shared instance memory shared_buffers, wal_buffers, lock/shared structures Allocated for the instance; many backends use it cooperatively.
Backend/session private memory Process/session overhead, caches and local contexts Potentially one set per connected backend.
Query-operation memory work_mem for sorts/hash tables; hash allowance affected by hash_mem_multiplier Potentially multiple operations per query × many concurrent queries.
Maintenance memory maintenance_work_mem, autovacuum_work_mem Per maintenance operation/worker under their own concurrency constraints.

2. shared_buffers: important, but not all cache

shared_buffers is PostgreSQL’s shared buffer cache. Data pages used by backends pass through this shared area, but PostgreSQL also relies on operating-system caching. Increasing shared_buffers changes memory allocation and can affect checkpoint/write behavior; it is a postmaster-context setting that requires restart.

sql · inspect shared buffer configuration
SELECT name, setting, unit, context, source, pending_restartFROM pg_catalog.pg_settingsWHERE name IN ('shared_buffers','huge_pages','max_connections')ORDER BY name;

The official documentation offers starting-point guidance for dedicated servers, but it is not a law. Container limits, mixed workloads, kernel behavior, checkpoint settings, dataset size, read/write mix, and co-located services can all change the right choice. In this course, any percentage is a hypothesis to test, not a “best setting.”

3. wal_buffers: small by design and usually automatic

wal_buffers holds WAL data that has not yet been written to disk. With the default automatic value, PostgreSQL derives an amount from shared_buffers within documented limits. Because it is also a startup-time setting, casual live experimentation is a poor beginner tuning strategy.

sql · inspect WAL buffer settings
SELECT name, setting, unit, context, sourceFROM pg_catalog.pg_settingsWHERE name IN ('wal_buffers','wal_segment_size')ORDER BY name;

Do not confuse WAL buffers with the amount of WAL retained on disk, archive retention, replication slots, or max_wal_size. Those are separate mechanisms covered later.

4. work_mem is per operation, not per server and not simply per connection

work_mem is a base memory limit used by query operations such as sorts and hash tables before spilling to temporary disk files. A single query can contain several such operations. Several sessions can run those queries concurrently. Hash-based operations can have an effective limit based on work_mem × hash_mem_multiplier. This is why “100 connections × work_mem” is still only a rough lower-resolution model: plan shape matters.

sql · inspect work memory settings
SHOW work_mem;SHOW hash_mem_multiplier;SELECT name, setting, unit, context, sourceFROM pg_catalog.pg_settingsWHERE name IN ('work_mem','hash_mem_multiplier')ORDER BY name;
Important correction to a common shortcut

Do not say “PostgreSQL reserves work_mem for every connection.” It does not pre-reserve the full amount per connection. The risk is that many simultaneously active plan nodes can each allocate toward their allowed memory at the same time.

5. Observe a sort that spills—on a disposable dataset

The ServiceHub seed from Chapter 01 is intentionally tiny, so it will not demonstrate memory spills. Create a temporary lab table or use generate_series to produce enough rows for one session. Keep the data disposable and measure your own result; do not copy timings from this lesson.

sql · create a temporary spill exercise
CREATE TEMP TABLE ch02_sort_lab ASSELECT g AS id,       md5(g::text || repeat('x', 20)) AS payloadFROM generate_series(1, 250000) AS g;ANALYZE ch02_sort_lab;SET work_mem = '1MB';EXPLAIN (ANALYZE, BUFFERS)SELECT *FROM ch02_sort_labORDER BY payload;RESET work_mem;

Run this inside an explicit transaction if you want SET LOCAL to apply only until commit/rollback:

sql · transaction-local experiment
BEGIN;SET LOCAL work_mem = '1MB';EXPLAIN (ANALYZE, BUFFERS)SELECT * FROM ch02_sort_lab ORDER BY payload;ROLLBACK;

Look for the sort node and its method. If the sort spills, PostgreSQL can report an external sort method and disk usage. Your exact result depends on row width, platform, PostgreSQL build, available memory, and plan. If it does not spill, reduce work_mem only within safe allowed limits or increase the disposable row count—do not pretend an expected spill occurred.

6. Temporary-file logging can make spills observable across sessions

EXPLAIN ANALYZE is excellent for one measured statement, but production diagnosis also needs fleet-level evidence. PostgreSQL can log temporary files above a configured size through log_temp_files. That setting has operational noise implications, so do not enable aggressive logging globally without considering log volume.

sql · inspect temp-file logging configuration
SELECT name, setting, unit, context, sourceFROM pg_catalog.pg_settingsWHERE name IN ('log_temp_files','temp_file_limit')ORDER BY name;

Lesson 5 develops the logging subsystem; here the key idea is that query memory decisions should be tied to observed spill frequency, sizes, concurrency, and latency, not isolated microbenchmarks.

7. maintenance_work_mem and autovacuum memory

maintenance_work_mem is used by maintenance operations such as VACUUM and CREATE INDEX. Because one session normally runs one such maintenance operation at a time, administrators often set it higher than work_mem. But autovacuum introduces its own concurrency: multiple autovacuum workers may each allocate maintenance memory unless autovacuum_work_mem controls them separately.

sql · inspect maintenance memory and worker concurrency
SELECT name, setting, unit, context, sourceFROM pg_catalog.pg_settingsWHERE name IN (  'maintenance_work_mem',  'autovacuum_work_mem',  'autovacuum_max_workers')ORDER BY name;

Do not multiply every setting by every connection blindly. Build a scenario model: maximum concurrent analytical queries × heavy plan nodes; maximum simultaneous maintenance workers; application idle/active connection split; shared allocations; OS and co-located services. Then validate with workload evidence.

8. A practical memory worksheet

The following is a planning framework, not a formula for exact peak RSS. PostgreSQL memory contexts, allocator behavior, parallel workers, JIT, extension code, plan shape, kernel caches, and workload timing make exact prediction more complex.

Budget line Question Conservative input
Host/container limit What memory is truly available to this PostgreSQL instance? Use the enforced cgroup/container/VM/host limit, not marketing RAM.
OS + filesystem + agents What must remain outside PostgreSQL allocations? Measure baseline and leave headroom.
Shared PostgreSQL memory What startup allocations are configured? Record shared_buffers, WAL/shared structures.
Backend baseline How many connected/active backends and what is their observed overhead? Measure under representative connection states.
Concurrent query operations How many memory-heavy nodes may run at once? Use plans + concurrency, not max_connections alone.
Maintenance/background How many VACUUM/index/autovacuum workers can overlap? Use configured worker capacity and schedules.
Safety headroom What happens during traffic spikes or plan changes? Keep explicit reserve; do not plan to 100% utilization.

9. Deliberately wrong approach: set global work_mem to 256MB for “faster sorts”

Suppose one report spills to disk with work_mem = 4MB. An operator raises the cluster-wide default to 256MB because the report becomes faster in an isolated test. That ignores concurrency and plan multiplicity. Ten concurrent reports with several hash/sort nodes can create a radically different memory footprint, and ordinary OLTP sessions inherit the same high default.

A safer repair is targeted experimentation: measure the plan, choose a session/transaction-local override for the known report or role/database default only after capacity analysis, load test at expected concurrency, and preserve headroom.

sql · targeted transaction-local memory experiment
BEGIN;SET LOCAL work_mem = '32MB';EXPLAIN (ANALYZE, BUFFERS)SELECT customer_id, count(*)FROM app.work_ordersGROUP BY customer_idORDER BY count(*) DESC;ROLLBACK;

On the tiny Chapter 01 dataset this query will not need 32MB; that is the lesson. Increasing a limit that the query does not need proves nothing about production sizing.

10. Hands-on lab: compare low and moderate work_mem safely

  1. Record shared_buffers, wal_buffers, work_mem, hash_mem_multiplier, maintenance_work_mem, autovacuum_work_mem, and max_connections.
  2. Create the temporary ch02_sort_lab table.
  3. Run the same EXPLAIN (ANALYZE, BUFFERS) sort in two transactions using two different SET LOCAL work_mem values.
  4. Record sort method, disk spill if present, execution time, and buffer evidence. Treat them as local observations only.
  5. Drop/disconnect; the temporary table disappears automatically with the session.
  6. Write a short capacity note explaining why you would not simply apply the larger value cluster-wide.

Check your understanding

  1. Why is work_mem not pre-reserved once per connection?
  2. How can one query use several work_mem-bounded allocations?
  3. Why can autovacuum make maintenance_work_mem budgeting surprising?
  4. What evidence can show a sort spilled to disk?
  5. Why is a fixed percentage for shared_buffers only a starting hypothesis?
Review the answers

work_mem is an execution limit used by eligible plan operations as they run, and one plan may contain several sorts/hashes while many sessions execute concurrently. Autovacuum can run multiple workers, so maintenance memory can multiply unless separately constrained. EXPLAIN can expose sort method/disk usage, while temp-file logs can provide broader evidence. Finally, PostgreSQL shares caching responsibility with the operating system and workloads differ, so shared-buffer sizing must be validated for the actual environment.

11. Production judgment and next bridge

Memory tuning belongs after workload characterization. Track active connections, plans, temp-file activity, OOM events, host/container memory, swap behavior where applicable, and latency. Use targeted role/session settings for exceptional workloads when justified rather than inflating global defaults.

Lesson 4 explains exactly how those global, database, role, session, and transaction-local settings interact—and how pg_settings.source/context/pending_restart keep the change auditable.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.