PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 26: Memory and configuration

Running PostgreSQL on Linux: huge pages, OOM, kernel settings and memory contexts

Level 5: Internals30 min

PostgreSQL relies on the operating system for memory, files and I/O, and several Linux defaults are wrong for a database server. This lesson covers huge pages, memory overcommit and the OOM killer, the kernel and systemd settings that matter, shutdown modes, and how to see where a backend's memory goes.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Size and enable huge pages for shared memory
  • Configure overcommit and OOM protection so one backend cannot take down the server
  • Inspect a backend's memory contexts and follow long operations through progress views

conceptThe concept

  • Huge pages: shared memory mapped in 2 MB pages instead of 4 kB shrinks page tables per backend (hundreds of MB with many connections) and reduces TLB misses. Set huge_pages = on (fail if unavailable) or try, and reserve vm.nr_hugepages from shared_memory_size_in_huge_pages. Disable transparent huge pages.
  • Overcommit and the OOM killer: by default Linux promises more memory than exists, then kills a process when it runs out. If it kills a backend, the postmaster assumes shared memory may be corrupted, terminates every session and runs crash recovery. Use vm.overcommit_memory = 2 so allocations fail cleanly with "out of memory" in one session, and protect the postmaster with an OOM score.
  • Kernel and limits: open file limit (LimitNOFILE, max_files_per_process), vm.swappiness low, dirty-page writeback in bytes rather than percentages, XFS or ext4 with noatime.
  • systemd: Type=notify, KillMode=mixed, KillSignal=SIGINT (fast shutdown), and RemoveIPC=no in logind so logging out the postgres user does not delete shared memory.
  • Shutdown modes: smart (wait for clients), fast (roll back sessions, checkpoint, exit; the default), immediate (no checkpoint; crash recovery on next start).
  • Memory contexts: every backend allocates memory in a tree of contexts. pg_backend_memory_contexts shows your own session; pg_log_backend_memory_contexts(pid) writes another backend's tree to the server log.
  • Progress views: pg_stat_progress_vacuum, _analyze, _create_index, _cluster, _copy and _basebackup show phase and blocks done for long operations.

whyWhy it matters

Many "PostgreSQL crashed" incidents are the OOM killer, and many "PostgreSQL is slow with 1000 connections" incidents are page-table memory without huge pages. Both are fixed in the OS, not in postgresql.conf. When a single query eats memory, memory contexts tell you which part of execution holds it.

Use caseReal-world scenario

A reporting query with a large hash join pushes a 64 GB server over its memory. With default overcommit the kernel kills a random backend, every application connection drops, and the server spends two minutes in crash recovery. After setting vm.overcommit_memory = 2, the same query fails alone with "out of memory" while every other session continues; pg_log_backend_memory_contexts on the offender shows a 9 GB HashTableContext, and the team lowers hash_mem_multiplier for the reporting role.

Try itExamples

Run and edit these in the interactive lesson.

Shared memory and huge pages
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('shared_buffers', 'shared_memory_size', 'shared_memory_size_in_huge_pages',
               'huge_pages', 'huge_pages_status', 'huge_page_size', 'shared_memory_type')
ORDER BY name;
On a real server run postgres -D $PGDATA -C shared_memory_size_in_huge_pages before starting, and set vm.nr_hugepages to at least that value.
Where this backend's memory is
SELECT name, level, pg_size_pretty(total_bytes) AS total, pg_size_pretty(used_bytes) AS used
FROM pg_backend_memory_contexts
ORDER BY total_bytes DESC
LIMIT 5;
SELECT pg_size_pretty(sum(total_bytes)) AS backend_total
FROM pg_backend_memory_contexts;
CacheMemoryContext holds the catalog caches; it grows with the number of tables and partitions a session touches, which is why per-connection memory matters with thousands of partitions.
Progress views for long operations
SELECT relname AS progress_view
FROM pg_class
WHERE relname LIKE 'pg_stat_progress%' AND relnamespace = 'pg_catalog'::regnamespace
ORDER BY relname;
SELECT pid, relid::regclass AS index_on, phase, blocks_done, blocks_total
FROM pg_stat_progress_create_index;
Empty here because nothing is running. During CREATE INDEX CONCURRENTLY on a large table, run this with \watch to see each phase and the block counts.
File and I/O settings
SELECT name, setting
FROM pg_settings
WHERE name IN ('max_files_per_process', 'io_method', 'io_workers', 'effective_io_concurrency',
               'maintenance_io_concurrency', 'data_directory_mode')
ORDER BY name;
max_files_per_process is per backend; with 500 connections the OS limit must cover all of them plus the postmaster.
The Linux side
# /etc/sysctl.d/30-postgresql.conf
vm.overcommit_memory = 2
vm.overcommit_ratio = 90            # CommitLimit = swap + 90% of RAM; tune to the machine
vm.swappiness = 1
vm.nr_hugepages = 4300              # from: postgres -D $PGDATA -C shared_memory_size_in_huge_pages
vm.dirty_background_bytes = 67108864
vm.dirty_bytes = 536870912
# kernel command line: transparent_hugepage=never

# systemd unit override for the server
[Service]
Type=notify
User=postgres
OOMScoreAdjust=-1000
Environment=PG_OOM_ADJUST_FILE=/proc/self/oom_score_adj
Environment=PG_OOM_ADJUST_VALUE=0
LimitNOFILE=65536
KillMode=mixed
KillSignal=SIGINT
TimeoutSec=infinity

# /etc/systemd/logind.conf
RemoveIPC=no

# shutdown modes
pg_ctl stop -D "$PGDATA" -m smart       # wait for every client to disconnect
pg_ctl stop -D "$PGDATA" -m fast        # default: roll back sessions, checkpoint, exit
pg_ctl stop -D "$PGDATA" -m immediate   # no checkpoint, crash recovery on next start

-- write another backend's memory context tree to the server log
SELECT pg_log_backend_memory_contexts(12345);

internalsUnder the hood

Shared memory (shared buffers, WAL buffers, lock tables) is one mapping created by the postmaster and inherited by every forked backend; each process needs its own page-table entries for it, which is what huge pages shrink. Private memory (work_mem for each sort or hash node, maintenance_work_mem, catalog caches) comes from memory contexts that are freed in bulk when the context is reset, so leaks are bounded by query or transaction lifetime. On Linux, huge_pages_status (17+) reports whether huge pages were actually obtained. PostgreSQL 18 adds asynchronous I/O (io_method: worker by default, io_uring where supported, or sync).

Avoid / preferDo this, not that

Avoid
# default vm.overcommit_memory = 0, no OOM protection
Prefer
vm.overcommit_memory = 2
# plus OOMScoreAdjust=-1000 for the postmaster and PG_OOM_ADJUST_VALUE=0 for backends

When the OOM killer kills any backend, the postmaster terminates every session and runs crash recovery. With strict overcommit the allocation fails inside one query, which gets an error while everything else continues.

Avoid
kill -9 12345   # a stuck backend
Prefer
SELECT pg_cancel_backend(12345);      -- cancel the query
SELECT pg_terminate_backend(12345);   -- end the session

SIGKILL on any server process is treated as a crash: the postmaster resets shared memory and disconnects everyone. The SQL functions stop just that session cleanly.

Avoid
huge_pages = on   # without reserving vm.nr_hugepages
Prefer
# reserve first, then
huge_pages = on   # or try, and check huge_pages_status after start

With on and no reserved pages the server refuses to start. With try it silently falls back to 4 kB pages; huge_pages_status (17+) tells you which happened.

PitfallsCommon mistakes

  • Leaving transparent huge pages enabled, which causes latency spikes from background compaction.
  • Setting overcommit_memory = 2 with a ratio too low for the machine, so the server cannot start or connections fail.
  • Using immediate shutdown routinely; it skips the checkpoint and forces crash recovery.
  • Forgetting RemoveIPC=no, so the server breaks when someone logs out of the postgres account.

LimitsWhen not to use it

On managed services (RDS, Cloud SQL, Azure) you cannot change kernel settings; huge pages and OOM behaviour are the provider's job. In Kubernetes, the container memory limit replaces overcommit settings: size the limit above shared_buffers plus max_connections times realistic work_mem, or the cgroup OOM killer triggers the same crash-restart cycle.

SecuritySecurity notes

The data directory must be owned by the postgres OS user with mode 0700 (or 0750 with group access); the server refuses to start otherwise. pg_log_backend_memory_contexts is superuser-only by default because the log can reveal query text; grant EXECUTE on it only to operations roles.

ChallengeTry it yourself

List the five largest memory contexts in this backend that are more than half used, with total size in kB and the percentage used.

Show one solution
One solution
SELECT name, total_bytes / 1024 AS total_kb,
       round(100.0 * used_bytes / total_bytes) AS pct_used
FROM pg_backend_memory_contexts
WHERE used_bytes * 2 > total_bytes
ORDER BY total_bytes DESC
LIMIT 5;

InterviewQuestions you should be able to answer

The OOM killer killed a PostgreSQL backend. What happened next, and how do you prevent it?
The postmaster saw a child die abnormally, assumed shared memory might be corrupted, terminated all other backends and ran crash recovery, so every client was disconnected. Prevention: vm.overcommit_memory = 2 with a suitable overcommit_ratio so allocations fail inside one query; OOMScoreAdjust=-1000 on the postmaster with PG_OOM_ADJUST_FILE/VALUE so backends remain killable; and keep total memory (shared_buffers + connections times work_mem) within RAM. In Kubernetes, set the memory limit accordingly.
Why use huge pages for PostgreSQL?
Each backend maps the whole shared memory segment and needs page-table entries for it. With 4 kB pages and large shared_buffers, page tables across hundreds of backends cost gigabytes of RAM and many TLB misses. 2 MB huge pages cut that by about 500 times. Reserve them with vm.nr_hugepages sized from shared_memory_size_in_huge_pages, set huge_pages = on, disable transparent huge pages, and confirm with huge_pages_status.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation