Why Huge Pages Matter for Postgres?

saisrirampur1 pts0 comments

. -->

How we configure huge pages in ClickHouse Managed Postgres | ClickHouse<br>Skip to content

Open searchOpen region selectorEnglish<br>Japanese

48.6kSign inGet Started

->Scroll to top<br>BackBlog<br>Engineering<br>Copy pageCopied!More actionsView as Markdown Open this page in Markdown<br>Open in ChatGPT Ask questions about this page<br>Open in Claude Ask questions about this page<br>Open in v0 Ask questions about this page

How we configure huge pages in ClickHouse Managed Postgres

Kaushik Iska<br>Jul 14, 2026 · 6 minutes read

What are huge pages, and why do they matter for Postgres? The OS hands out memory in 4KB pages, and the CPU keeps a small cache of virtual-to-physical translations, the TLB. A huge page is the same memory in much bigger units, 2MB or 1GB, so one page-table entry covers up to 262,144x more ground.

Postgres is unusually sensitive to page size. Its cache, shared_buffers, is one big shared-memory segment, and every backend process maps it through its own page tables. At 4KB pages, a 100GB cache costs each backend about 200MB of page-table entries, so 100 connections burns 20GB of RAM on page tables alone; with 2MB huge pages, tens of MB. And a TLB holds a few thousand translations, which covers just a few MB of that cache at 4KB, so each 2MB page gives a slot 512x the reach, the hot working set stays in the TLB, and reads stop stalling on page-table walks.

In ClickHouse Managed Postgres every server runs its buffer cache on huge pages. Getting that to hold reliably takes three pieces: reserving the pages early, refusing to run without them, and sizing shared_buffers so the whole shared memory segment lands on the reserved pool exactly.

Reserving the pages #

A quarter of the machine's memory is reserved as huge pages, mirroring the shared_buffers = 25% of RAM rule in the base configuration. The reservation runs before Postgres ever starts, and it flushes caches and compacts memory first so the kernel can still find contiguous 2MB blocks:

1echo 'vm.nr_hugepages = ' > /etc/sysctl.d/10-hugepages.conf<br>2sync<br>3echo 3 > /proc/sys/vm/drop_caches<br>4echo 1 > /proc/sys/vm/compact_memory<br>5sysctl --systemCopy command

The allocation is then verified against HugePages_Total in /proc/meminfo, and provisioning fails if the kernel comes up short. Huge pages can only be reserved reliably while memory is unfragmented; on a machine that has been under load, the same request routinely fails.

Refusing to run without them #

Postgres is pinned to huge_pages = 'on', so it either gets its pages or refuses to start. The default, 'try', silently falls back to 4KB pages with nothing in the logs, and every property described above quietly disappears. A refusal at startup is diagnosable; a silent fallback is discovered months later, usually by a profiler.

There is a runtime check, too. The postmaster's /proc//status records how much of its address space sits on hugetlb pages; on the demo box below it reads:

1$ grep HugetlbPages /proc//status<br>2HugetlbPages: 700416 kBCopy command

HugetlbPages: 0 on a server that should be using huge pages means the fallback happened.

Sizing shared_buffers to the page #

Postgres's shared memory segment is bigger than shared_buffers: it also carries buffer descriptors, WAL buffers, and lock tables. On the demo box below, shared_buffers = 32GB produces a 32.75GB segment, about 750MB of overhead. If shared_buffers alone filled the reserved pool, the full segment would not fit, and 'on' would refuse to boot. The sizing works in two passes. First point shared_buffers at the entire pool, then ask the binary what the full segment would actually cost, without starting the server:

1postgres -D -C shared_memory_sizeCopy command

Subtract the difference back out of shared_buffers, round down to whole 8KB blocks, and write the result:

1# conf.d/002-hugepages.conf<br>2huge_pages = 'on'<br>3huge_page_size = 0 # use the system default, 2MB<br>4shared_buffers = kBCopy command

The overhead is measured rather than assumed because it shifts with version and settings, and measuring at the larger size errs a few pages under the reservation. The reserved pool and the rest of memory are then accounted separately: huge pages do not count toward the kernel's commit limit, so memory overcommit is tuned against the remaining 75% of RAM.

Seeing it on real hardware #

We put the claim about page tables on a single EC2 box: an r7i.4xlarge (16 vCPU, 128GB), Postgres 16 with shared_buffers = 32GB, and a pgbench dataset whose 15.6GB accounts table is loaded fully into the cache with pg_prewarm. Each test connection then runs one full sequential scan of that table, which makes its backend touch every cached page and build its complete page-table mapping, and holds the connection open. We sampled the kernel's PageTables counter from /proc/meminfo at 25, 50, 100, and 200 connections, once with 4KB pages and once with 2MB huge pages. Same box, same data, same access pattern; the only variable is the page size.

With 4KB pages, page...

pages page huge postgres memory shared_buffers

Related Articles