shared_buffers
Sets the shared memory pool for caching data pages. Too small: frequent disk reads. Too large: OOM or less OS cache for reads.
Tier 2 — Production Guide — Operational guidance is available; evidence scope is stated where relevant.
What We Observed
box
(PG 17.10, 128MB shared_buffers)
table
sb_test 500k rows; sb_wl 100k rows
cold cache
- setting
128MB (default)
- method
DISCARD ALL (session cache cleared)
- plan
Index Scan on sb_test
- exec time ms
32.523
- source file
warm cache
- setting
128MB (same, but pg_prewarm loaded table into cache)
- method
pg_prewarm('sb_test') then EXPLAIN ANALYZE
- plan
Index Scan on sb_test (all from shared_buffers)
- exec time ms
19.198
- source file
analytics cold
- exec time ms
2094.268
- buffers
shared hit=1 read=7160
- source file
analytics warm
- exec time ms
145.267
- buffers
shared hit=7161 read=0
- speedup
14.4x vs cold
- source file
pg buffercache
- total
128MB
- used by relations
46-49MB
- pct used
36-38%
- source file
curve note
Cache hit ratio improves with more buffers until working set fits. Returns flatten once full dataset is resident. Cold→warm speedup = 14.4× on 500k-row analytics sort.
What Was Expected
- framing
Two distinct dangers: (1) startup failure if shared_buffers exceeds kernel/container memory limits; (2) performance regression from double-buffering when shared_buffers takes too much RAM away from OS page cache.
- danger 1 startup
- setting
2GB on 512MB container
- container
cl_sb_danger (--memory=512m --memory-swap=512m)
- result
Running=true ExitCode=0 OOMKilled=false — modern PG (9.3+) uses mmap; startup does not FATAL, but memory pressure causes OOM under load
- historical note
In PG < 9.3 (System V shmem), startup FAILs immediately. PG 9.3+ with mmap: startup succeeds; OOM happens at first heavy access.
- source file
- danger 2 performance
- setting
Too high (>50% RAM) → OS page cache evicted → double-buffering: same data in shared_buffers AND kernel page cache
- result
Diminishing returns; additional RAM gives no speedup; checkpoint flush time increases
- evidence
cold (2094ms) vs warm (145ms) analytics benchmark shows how critical cache residency is
- source file
- safe range
15-30% of total RAM as starting point; measure hit ratio with pg_buffercache
- source file
Common Pitfalls
- Believing 'the default changed from 8MB to 128MB across versions'. The EFFECTIVE default is 128MB on all of 14-18 (initdb auto-tunes it); only the compiled boot_val fallback differs (PG14 8MB). Don't quote boot_val as the default.
- Setting shared_buffers near 100% of RAM. It starves the OS page cache (PostgreSQL's L2) and the work_mem/connection budget; oversizing either blocks startup (could-not-map FATAL) or causes a latent OOM under load.
- Expecting big shared_buffers to speed up large sequential scans. It won't until shared_buffers exceeds ~4x the table — large seqscans use a 256kB ring buffer (BAS_BULKREAD) by design and rely on the OS cache.
- Forgetting it needs a RESTART. ALTER SYSTEM + pg_reload_conf leaves pending_restart=true; the new value only takes effect after a server bounce — plan the outage.
- Sizing far past the hot working set. The measured curve is flat (even slightly worse) once the working set fits; extra shared_buffers is wasted RAM that the OS cache could have used.
- Treating shared_buffers as 'how much data fits in RAM'. The OS cache already holds most warm data; shared_buffers is about hit-ratio, write coalescing, and latency consistency, not total caching.
- Changing it without a tested revert value. A too-high value can prevent the server from restarting at all, locking you out until you edit the config back down.
Tuning Guidance
Unlock Incident Pro
The free sections tell you what to do. Pro is the measured proof — the exact failure signature, safe-ceiling formula, and per-workload defaults, each captured on real PG 14–18 runs.
- Tuning Guidance
shared_buffers is the size of PostgreSQL's own shared-memory page cache: the pool of 8kB buffers in which it keeps table and index pages it has read or written. Every backend reads and writes through this pool. It is allocated ONCE at server start (context=postmaster) and cannot change without a restart. Crucially it is NOT PostgreSQL's only cache: the operating system's page cache sits underneath it, so a page evicted from shared_buffers is usually still in OS RAM and re-read cheaply. That two-tier reality is why shared_buffers tuning is about hit-ratio and write coalescing, not 'put all my data in RAM' — and why setting it to a huge fraction of RAM backfires by starving the OS cache.
Measured literally from pg_settings on PG 14/15/16/17/18: setting=16384, unit=8kB => effective default 128MB on ALL FIVE versions (reset_val=16384 everywhere). vartype=integer, min_val=16 (8kB pages => 128kB floor), max_val=1073741823. context='postmaster' => requires a full server RESTART to change; a wrong value blocks startup. HONESTY NUANCE captured by the matrix: boot_val (compiled-in fallback) is 1024 (8MB) on PG14 but 16384 (128MB) on PG15-18 — so PG15 raised the COMPILED fallback, NOT the effective default. A normally created cluster shows 128MB on every version because initdb probes the box and writes a tuned shared_buffers into postgresql.conf at cluster creation. Therefore the correct statement is 'effective default 128MB across 14-18'; the often-repeated 'default changed from 8MB to 128MB' confuses boot_val with the real default.
shared_buffers is structurally identical across PG 14-18: effective default 128MB, min 128kB, max ~8TB, context=postmaster, unit=8kB (see _guc_matrix_machine for literal per-version rows). The ONLY cross-version difference is boot_val: PG14=1024 (8MB compiled fallback) vs PG15-18=16384 (128MB) — visible only on a cluster started with no configured shared_buffers and no initdb auto-tuning, which is not the normal case. effective_cache_size (the related PLANNER hint, default 4GB here) is also identical in shape across versions. Bottom line: treat '128MB default, postmaster context' as constant from 14 through 18.
context=postmaster, so applying a change is a TWO-STEP, restart-required operation: ALTER SYSTEM SET shared_buffers = '4GB'; then RESTART the server (pg_ctl restart / systemctl restart postgresql / docker restart) — SELECT pg_reload_conf(); is NOT enough and the change shows pending_restart=true until you bounce. Verify with SHOW shared_buffers; after restart. There is no per-session or per-role override (unlike work_mem) — it is one cluster-wide value. Because the restart is a brief outage and a bad value can prevent the server from coming back up (see danger), change shared_buffers in a maintenance window and keep the previous known-good value to revert to. BOUNDARY PROOF (captured literal): an out-of-range / invalid value is rejected at set time -- ALTER SYSTEM SET shared_buffers = 15; -> ERROR: 15 8kB is outside the valid range for parameter "shared_buffers" (16 8kB .. 1073741823 8kB). It is context=postmaster, so a session-level set is refused -- SET shared_buffers = ... -> ERROR: parameter "shared_buffers" cannot be changed without restarting the server -- and ALTER SYSTEM leaves pg_settings.pending_restart=t until the server is restarted.
References
- PostgreSQL 18 docs — Resource Consumption / Memory: shared_buffers, effective_cache_size, huge_pages (postgresql.org/docs/18/runtime-config-resource.html)
- PostgreSQL source — buffer ring / BAS_BULKREAD strategy for large scans (src/backend/storage/buffer/freelist.c; GetAccessStrategy / 256kB ring) — observed via pg_buffercache
- PostgreSQL 18 docs — pg_buffercache and pg_prewarm extensions (postgresql.org/docs/18/pgbuffercache.html, /pgprewarm.html)
- PostgreSQL 18 docs — pg_settings (boot_val vs reset_val semantics) (postgresql.org/docs/18/view-pg-settings.html)
- Lab ground truth: truth/pg_settings_pg14.18.tsv (this repo)
- Lab captures: sb_curve.txt, sb_hitratio.txt, sb_danger.txt} (this repo)
Technical Reference
- Category
- Resource Usage / Memory
- Type
- integer (8kB)
- Blast radius
- cluster
- Severity
- High
- Context
- postmaster
- Versions
- 14, 15, 16, 17, 18
- Reviewed
- 2026-06-19
Severity scale: 1 Minimal · 2 Low · 3 Moderate · 4 High · 5 Critical.