Linux manages memory in 4 KB pages by default. A database with a 16 GB shared buffer pool therefore needs about four million page table entries, and the CPU's translation lookaside buffer (TLB) can only cache a tiny fraction of them. Huge pages (2 MB on x86_64) cut that number by 512, which reduces TLB misses, shrinks page tables for every backend process and prevents the shared memory from ever being swapped out.
In this tutorial you will size and reserve a huge page pool for PostgreSQL 16 on Ubuntu 24.04, make PostgreSQL require it, and verify it is in use. A shorter section covers MySQL 8.0, and another explains how transparent huge pages differ.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS on x86_64 with at least 4 GB of RAM. The examples use a server with 8 GB, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - PostgreSQL 16 installed from the Ubuntu repositories (
sudo apt install postgresql). The paths below use the default16/maincluster.
Huge pages give the biggest benefit when shared buffers are several gigabytes. On a small database with a 128 MB buffer pool the difference is negligible.
How huge pages work
Two different mechanisms share the name:
| Mechanism | How it works | Use for databases |
|---|---|---|
| Explicit huge pages (hugetlbfs) | A fixed pool reserved in advance with vm.nr_hugepages. Applications request it explicitly. Memory is never swapped. | Recommended for PostgreSQL, MySQL and Oracle. |
| Transparent huge pages (THP) | The kernel tries to merge regular pages into huge pages automatically in the background. | Can cause latency spikes from compaction. Keep it at madvise or never for most databases. |
Reserved huge pages are taken out of general memory at once, whether or not the database uses them. Size the pool to what the database needs, no more.
Step 1 - Checking the current state
Check the huge page size and pool on your system:
grep -i huge /proc/meminfo
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
HugePages_Total: 0 means no pool is reserved yet, and Hugepagesize: 2048 kB confirms 2 MB pages.
Check the transparent huge pages mode. Ubuntu defaults to madvise, which only uses THP for applications that ask for it:
cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never
Step 2 - Setting PostgreSQL shared buffers
The huge page pool must cover PostgreSQL's whole shared memory segment, which is mainly shared_buffers. A common starting point is 25% of RAM. Edit the configuration:
sudo nano /etc/postgresql/16/main/postgresql.conf
Set the buffer size and keep huge pages optional for now, so PostgreSQL still starts before the pool exists:
shared_buffers = 2GB
huge_pages = try
Restart PostgreSQL to apply the change:
sudo systemctl restart postgresql@16-main
Step 3 - Calculating the number of huge pages
Since version 15, PostgreSQL reports exactly how many huge pages its shared memory needs, including overhead beyond shared_buffers. Ask it:
sudo -u postgres psql -c "SHOW shared_memory_size_in_huge_pages;"
shared_memory_size_in_huge_pages
----------------------------------
1081
(1 row)
With shared_buffers = 2GB, PostgreSQL needs 1081 pages of 2 MB. Add a small margin (about 2%) for rounding and extensions; this example reserves 1100 pages, about 2.1 GB.
Step 4 - Reserving the huge page pool
Reserve the pool persistently with a sysctl file:
sudo nano /etc/sysctl.d/60-hugepages.conf
vm.nr_hugepages = 1100
Apply it without a reboot:
sudo sysctl --system
grep -E 'HugePages_(Total|Free)' /proc/meminfo
HugePages_Total: 1100
HugePages_Free: 1100
If HugePages_Total is lower than requested, the kernel could not find enough contiguous memory because RAM is fragmented. Reboot and the pool will be allocated at startup, when memory is still clean.
Step 5 - Requiring huge pages in PostgreSQL
With huge_pages = try, PostgreSQL silently falls back to normal pages if the pool is too small. Switch to on so it refuses to start instead, which turns a silent performance regression into a visible error:
sudo nano /etc/postgresql/16/main/postgresql.conf
huge_pages = on
Restart PostgreSQL:
sudo systemctl restart postgresql@16-main
systemctl status postgresql@16-main --no-pager
● [email protected] - PostgreSQL Cluster 16-main
Active: active (running) since ...
Step 6 - Verifying that PostgreSQL uses huge pages
Look at the pool again:
grep -E 'HugePages_(Total|Free|Rsvd)' /proc/meminfo
HugePages_Total: 1100
HugePages_Free: 1052
HugePages_Rsvd: 1033
PostgreSQL has claimed its pages: HugePages_Rsvd counts pages reserved for the mapping but not yet touched, and HugePages_Free drops as the buffer pool fills. After the database has been running under load for a while, HugePages_Free approaches the margin you added.
You can also confirm it from the PostgreSQL side. If huge pages were not available, the server would not have started with huge_pages = on, and the log would contain the reason:
sudo tail -n 20 /var/log/postgresql/postgresql-16-main.log
Step 7 - Configuring MySQL 8.0 (optional)
MySQL allocates the InnoDB buffer pool with System V shared memory, so it needs two extra permissions: the mysql group must be allowed to use huge pages, and the service must be allowed to lock memory.
Find the mysql group ID:
getent group mysql
mysql:x:113:
Size the pool for your innodb_buffer_pool_size plus about 5% overhead (for a 2 GB buffer pool, about 1100 pages) and add the group ID:
sudo nano /etc/sysctl.d/60-hugepages.conf
vm.nr_hugepages = 1100
vm.hugetlb_shm_group = 113
sudo sysctl --system
Allow the MySQL service to lock memory:
sudo systemctl edit mysql.service
[Service]
LimitMEMLOCK=infinity
Enable large pages in the MySQL configuration:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
large_pages = ON
innodb_buffer_pool_size = 2G
Restart MySQL and check the variable and the pool:
sudo systemctl restart mysql
sudo mysql -e "SHOW VARIABLES LIKE 'large_pages';"
grep -E 'HugePages_(Total|Free)' /proc/meminfo
If MySQL cannot get huge pages, it logs a warning in /var/log/mysql/error.log and continues with normal memory, so always check that HugePages_Free decreased.
Step 8 - Keeping transparent huge pages out of the way
The explicit pool does not depend on THP, but THP in always mode can cause latency spikes when the kernel compacts memory, and Redis warns about it at startup. Ubuntu's default madvise is fine for PostgreSQL and MySQL. If you run Redis on the same server and want THP fully off, disable it at boot:
sudo nano /etc/default/grub
Append the parameter to GRUB_CMDLINE_LINUX_DEFAULT, keeping any existing values:
GRUB_CMDLINE_LINUX_DEFAULT="transparent_hugepage=never"
sudo update-grub
sudo reboot
After the reboot, cat /sys/kernel/mm/transparent_hugepage/enabled shows [never].
Troubleshooting
PostgreSQL fails to start with could not map anonymous shared memory: Cannot allocate memory. The pool is smaller than shared_memory_size_in_huge_pages, often because you increased shared_buffers later. Set huge_pages = try, start PostgreSQL, read the new value with SHOW, raise vm.nr_hugepages and switch back to on.
HugePages_Total is lower than vm.nr_hugepages. Memory is fragmented. Reboot so the pool is reserved early, or reserve it at boot with the kernel parameter hugepages=1100.
The server runs out of memory after enabling huge pages. Reserved pages are unavailable to everything else, even when unused. Check that vm.nr_hugepages times 2 MB leaves enough RAM for the operating system, database connections (work_mem) and the page cache.
HugePages_Free equals HugePages_Total while the database runs. The database is not using the pool. For PostgreSQL, set huge_pages = on to make the failure visible. For MySQL, check vm.hugetlb_shm_group and LimitMEMLOCK.
Conclusion
You calculated the exact huge page requirement from PostgreSQL, reserved a persistent pool with sysctl, made PostgreSQL refuse to start without it and verified the pages are in use, with the equivalent setup for MySQL. Whenever you change shared_buffers or innodb_buffer_pool_size, recalculate the pool. Next steps are tuning the rest of PostgreSQL's memory settings (effective_cache_size, work_mem), and on multi-socket servers checking NUMA placement with numactl --hardware.
