Transparent HugePages (THP) is a Linux memory feature. Normally your programs use small 4 KiB memory pages. THP lets the kernel group those into larger 2 MiB pages when it helps—mainly to speed up programs that scan a lot of RAM. The trade-off is extra work in the kernel (rearranging memory) and surprises for apps that fork (like Redis during a background save).
This guide answers a common question: should you disable THP? You will learn what the enabled and defrag settings do, how always, madvise, and never differ, and what I measured on Ubuntu 25.04.
For how THP differs from manually reserved HugePages (vm.nr_hugepages), see HugePages vs Transparent HugePages. To check whether THP is in use, see check Transparent HugePages in Linux.
Tested on: Ubuntu 25.04 (Plucky Puffin); kernel 7.0.0-27-generic.
madvise is the default on this Ubuntu install).
Quick answer: should you disable THP?
| Situation | What I would do |
|---|---|
| Normal Ubuntu / RHEL server or desktop | Leave the default (madvise on both knobs) unless you see a real problem |
| Redis, or a database on its own server where the vendor says turn THP off | Set enabled and defrag to never |
PostgreSQL with huge_pages=on |
Turn THP off and size static HugePages separately — configure HugePages |
| Big batch job that reads a huge chunk of RAM in order | Try madvise or test always; confirm with AnonHugePages |
| You read “always disable THP” on an old blog | Check your kernel default first — modern madvise is not the same as old always |
You turn THP off mainly for predictable latency on databases that fork and save to disk—not because huge pages are always bad.
When you are unsure:
- Start with your distro default (
madviseon Ubuntu 25.04) unless the app vendor names your version. - Move to
neveron bothenabledanddefragif you see slow saves, memory spikes during fork, or Redis-style warnings—not because of a generic rule from years ago. - Use static HugePages when the app documents them (PostgreSQL
huge_pages=on, some Kubernetes pods)—THP does not replace that.
Quick command summary
| Task | Command |
|---|---|
| THP mode | cat /sys/kernel/mm/transparent_hugepage/enabled |
| Defrag policy | cat /sys/kernel/mm/transparent_hugepage/defrag |
| THP page size | cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size (2097152 = 2 MiB) |
| System THP usage | grep -i AnonHugePages /proc/meminfo |
| Per-process THP | grep AnonHugePages /proc/PID/smaps_rollup |
| Set mode (runtime) | echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled |
| Disable THP (runtime) | echo never | sudo tee /sys/kernel/mm/transparent_hugepage/{enabled,defrag} |
| khugepaged running? | ps aux | grep khugepaged |
THP in one minute (and where to read more)
- Linux splits memory into pages (chunks). The usual size is 4 KiB. The CPU can only track a limited number of page translations at once; huge 2 MiB pages mean fewer lookups when you walk through a large array.
- Transparent means the kernel tries to do this for you—you do not reserve a special memory pool up front (that is static HugePages, a different feature).
- A background kernel thread called
khugepagedlooks for chances to merge small pages into big ones when policy allows. - The
defragsetting controls how hard the kernel shuffles free memory to find a contiguous 2 MiB block. Aggressive shuffling can slow down your program while it waits.
THP is not the same as HugePages_Total in /proc/meminfo or vm.nr_hugepages—those are for apps that ask for reserved huge pages (PostgreSQL, DPDK, some Kubernetes pods).
More depth: HugePages vs Transparent HugePages and Linux memory management overview.
Two sysfs knobs: enabled and defrag
Linux exposes THP settings as simple text files under /sys/kernel/mm/transparent_hugepage/. Two files matter day to day:
enabled— whether program memory may use transparent huge pages (always,madvise, ornever)defrag— how eagerly the kernel rearranges RAM to build a 2 MiB page
They work independently. You can leave enabled at madvise while defrag still changes how long allocations take—which is why database guides often mention both files.
enabled — who gets huge pages?
Read both files when you troubleshoot. The value in square brackets is what is active now:
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defragalways [madvise] never
always defer defer+madvise [madvise] neverOn this host enabled is madvise and defrag is madvise. The other words are values you can write with echo madvise | sudo tee ….
enabled value |
In plain English |
|---|---|
always |
The kernel may use huge pages for eligible program memory automatically |
madvise |
Huge pages are off by default; only programs that ask get them |
never |
No transparent huge pages for normal program memory |
What each mode feels like:
always— large long-lived programs (some JVM heaps, analytics jobs) may get 2 MiB pages without asking. Can speed up big memory scans, but uses memory in bigger chunks and can surprise fork-heavy apps.madvise— default on this host. Most programs stay on 4 KiB pages unless they opt in. For most general servers this behaves like “THP is off until something wants it.”never— THP is fully off for normal memory. Common on dedicated Redis nodes. Does not set up static HugePages—PostgreSQLhuge_pages=onstill needsvm.nr_hugepages(configure HugePages).
defrag — how hard the kernel fights for a 2 MiB block
Even when huge pages are allowed, the kernel needs 512 contiguous small pages (2 MiB) free together. Fragmented RAM may require compaction—moving other pages aside. The defrag file controls how much of that work happens while your program is waiting.
defrag value |
In plain English |
|---|---|
always |
May compact memory during your allocation—your thread can pause |
defer / defer+madvise |
More work happens later in the background (khugepaged) |
madvise |
Default here—aggressive work mainly when a program asked for huge pages |
never |
No compaction for THP |
Simple way to remember:
enabledanswers: “May this memory use huge pages?”defraganswers: “How hard should the kernel rearrange RAM to make that happen?”
With defrag=always, a single large allocation can block while the kernel moves pages. On this host that was 349 ms versus 136 ms for the same 512 MiB test with defrag=madvise (see benchmark below).
About khugepaged:
- Shows up in
ps auxwhen THP is allowed - Merges already-allocated small pages into big ones when the system is idle or under light load
- Heavy activity under load can line up with latency spikes—check before you tune
How the two settings work together
Many guides only mention enabled. When a database doc says “disable THP,” it usually means set both to never so neither merging nor compaction runs on the hot path.
| Goal | Typical enabled |
Typical defrag |
|---|---|---|
| Distro default (mixed server) | madvise |
madvise |
| Dedicated Redis / latency-sensitive DB | never |
never |
| Benchmarking maximum THP | always |
defer or madvise (avoid always unless you accept pauses) |
After you change either file, check grep AnonHugePages /proc/meminfo under real load—not only at idle. Per-process checks: check Transparent HugePages in Linux.
Check what is active today
On this host before testing:
grep -iE 'AnonHuge|HugePages_Total' /proc/meminfo
ps aux | grep '[k]hugepaged'AnonHugePages: 2048 kB
HugePages_Total: 0
root ... [khugepaged]How to read that output:
AnonHugePages: 2048 kB— a little THP is in use system-wide (normal on a running machine).HugePages_Total: 0— no reserved static huge-page pool; that number does not tell you whether THP is on or off.[khugepaged]running — the merge thread is available; withenabled=neverit does much less.
Full walkthrough: check Transparent HugePages in Linux.
Benchmark 1: enabled = never vs madvise vs always
I ran the same Python test on this host for each enabled mode (defrag=madvise throughout): allocate 256 MiB and ask for huge pages, touch every 4 KiB, then time a sequential read through the block.
# Example: set mode, then run workload (see sections below for scripts)
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabledenabled |
AnonHugePages after 256 MiB opt-in alloc |
Time to walk all pages |
|---|---|---|
never |
0 KiB (process) | 0.0203 s |
madvise |
208896 KiB (~204 MiB) | 0.0103 s |
always |
249856 KiB (~244 MiB) | 0.0193 s |
What that means on this host:
never— the program’s request for huge pages was ignored;AnonHugePagesstayed at 0. THP is fully off for this test.madvise— about 204 MiB of the 256 MiB block became huge pages; the full scan took about half as long asneveron this VM.always— even more memory used huge pages (~244 MiB), but the scan was not faster thanmadvisehere. System-widealwaysis not automatically a win.- Takeaway — huge pages can help when you read a lot of RAM in order, but you should measure on your workload instead of assuming
alwaysis best.
Allocation and page-walk check (enabled=madvise):
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
python3 <<'PY'
import ctypes, mmap, time
libc = ctypes.CDLL("libc.so.6")
size = 256 * 1024 * 1024
buf = mmap.mmap(-1, size, prot=mmap.PROT_READ|mmap.PROT_WRITE,
flags=mmap.MAP_PRIVATE|mmap.MAP_ANONYMOUS)
addr = ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(buf)))
libc.madvise(addr, size, 14) # MADV_HUGEPAGE
view = memoryview(buf)
for i in range(0, size, 4096):
view[i] = 1
with open("/proc/self/smaps_rollup") as f:
for line in f:
if line.startswith("AnonHugePages:"):
print(line.strip())
start = time.perf_counter()
s = sum(view[i] for i in range(0, size, 4096))
print(f"walk_seconds={time.perf_counter()-start:.4f}")
PYAnonHugePages: 208896 kB
walk_seconds=0.0103Benchmark 2: defrag impact on allocation latency
This test isolates defrag. I kept enabled=madvise (huge pages only when a program asks) and allocated 512 MiB twice—only defrag changed.
defrag |
Time to allocate and touch 512 MiB |
|---|---|
madvise (default) |
136 ms |
always |
349 ms |
What that means on this host:
enableddid not change — both runs usedmadvise. The slowdown comes fromdefrag, not from turning on system-wide huge pages.defrag=madvise(136 ms) — memory was set up without aggressive on-the-spot rearrangement.defrag=always(349 ms) — about 2.6× slower. The kernel may shuffle other pages while your program waits for a 2 MiB block.- Same result, different wait — both runs successfully used 512 MiB. The difference is how long you wait, not whether allocation works.
- Why DB guides care — hundreds of milliseconds on a large allocation can show up as query pauses or slow background saves. That is why “disable THP” often sets
defrag=neveras well asenabled=never.
To reproduce, run the script once with default defrag, then again after switching to always:
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
time python3 <<'PY'
import ctypes, mmap
libc = ctypes.CDLL("libc.so.6")
size = 512 * 1024 * 1024
buf = mmap.mmap(-1, size, prot=mmap.PROT_READ|mmap.PROT_WRITE,
flags=mmap.MAP_PRIVATE|mmap.MAP_ANONYMOUS)
addr = ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(buf)))
libc.madvise(addr, size, 14)
view = memoryview(buf)
for i in range(0, size, 4096):
view[i] = 1
PY
# restore defrag default:
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defragCompare the real time from time with defrag=madvise first—you should see a similar gap on a memory-loaded host.
Benchmark 3: fork and copy-on-write (Redis-style)
Redis background saves (BGSAVE) and similar tools fork a child process while the parent keeps a large memory area. When memory uses 2 MiB pages, copy-on-write (COW)—copying a page only when someone writes to it—can happen in bigger chunks than with 4 KiB pages. That can inflate memory use during the save window.
I filled 400 MiB with an opt-in huge-page request, waited for merging, forked, and had the child rewrite 80 MiB:
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
python3 <<'PY'
import ctypes, mmap, os, time
libc = ctypes.CDLL("libc.so.6")
size = 400 * 1024 * 1024
buf = mmap.mmap(-1, size, prot=mmap.PROT_READ|mmap.PROT_WRITE,
flags=mmap.MAP_PRIVATE|mmap.MAP_ANONYMOUS)
addr = ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(buf)))
libc.madvise(addr, size, 14)
view = memoryview(buf)
for i in range(0, size, 4096):
view[i] = 1
for _ in range(3):
time.sleep(1)
def ah():
with open("/proc/self/smaps_rollup") as f:
for line in f:
if line.startswith("AnonHugePages:"):
return line.strip()
print("parent_after_fill", ah())
pid = os.fork()
if pid == 0:
for i in range(0, 80*1024*1024, 4096):
view[i] = 2
print("child_after_cow", ah())
os._exit(0)
os.waitpid(pid, 0)
print("parent_after_child", ah())
PYenabled |
Parent AnonHugePages after fill |
Child AnonHugePages after COW |
|---|---|---|
never |
0 KiB | 0 KiB |
madvise |
409600 KiB (~400 MiB) | 327680 KiB (~320 MiB) |
always |
409600 KiB | 327680 KiB |
Example output with enabled=madvise:
parent_after_fill AnonHugePages: 409600 kB
child_after_cow AnonHugePages: 327680 kB
parent_after_child AnonHugePages: 409600 kBWith enabled=never, the same script reported AnonHugePages: 0 kB at each step.
What the fork test shows:
- Parent fills 400 MiB — with
madviseoralways, most of it becomes huge pages (~400 MiB inAnonHugePages) after a few seconds. - Child rewrites 80 MiB after fork — COW breaks sharing; the child can still hold hundreds of MiB as huge pages because copies happen in 2 MiB units when THP is active.
- With
never— the same pattern stays on small pages;AnonHugePagesstays at 0 KiB. - Redis angle — during BGSAVE, fork plus huge-page memory can spike RSS and lengthen the save even when normal traffic looks fine.
- Lab script, real mechanism — this is not Redis itself, but it shows why vendors warn about THP and fork-based persistence.
What to use for common workloads
| Workload | Suggested setting | Why in plain terms |
|---|---|---|
| General Linux VM / app server | enabled=madvise, defrag=madvise |
Huge pages only when a program asks—safe default |
| Redis (dedicated server) | enabled=never, defrag=never |
Fork + background save; Redis warns at startup if THP is too aggressive |
| PostgreSQL | THP off; static huge pages if you want them | Predictable reserved pool—configure HugePages |
| MySQL / MariaDB | Often never on dedicated DB servers |
Historical latency spikes from memory rearrangement |
| MongoDB | Check your version’s docs | Advice changed between releases |
| JVM / analytics | madvise or test always |
Large heaps may benefit if pauses are acceptable |
| Kubernetes worker | Node THP policy + static huge pages for pods that request them | THP ≠ hugepages-2Mi in the pod spec—comparison guide |
madvise as a fix—that stops system-wide always but still allows opt-in huge pages. Many operators set never on dedicated Redis hosts for simplicity.
Change THP at runtime
# Balanced (Ubuntu default on this host)
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
# Disable THP completely
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
# Verify
cat /sys/kernel/mm/transparent_hugepage/enabled
grep AnonHugePages /proc/meminfoAfter you write sysfs:
- Changes apply immediately to the whole machine—there is no per-app toggle in these files.
- Re-read
enabledanddefragand confirm the bracketed value. - Run your app, then check
AnonHugePages—idle systems may show 0 KiB even when THP is allowed. - Sysfs changes are lost on reboot unless you add GRUB, systemd, or
tunedpersistence (next section).
For related virtual-memory tuning (swap, not THP), see swap and swappiness guide and sysctl for high performance servers.
Make changes persistent (overview)
| Method | When to use |
|---|---|
GRUB transparent_hugepage=never |
Boot-wide policy on bare metal / VMs |
| systemd oneshot unit writing sysfs | Ubuntu/Debian without GRUB edits |
tuned profile |
RHEL-family managed hosts |
Choosing a persistence method:
- GRUB
transparent_hugepage=never— policy is set at boot before your services start; good when every workload on the box should share one rule. - systemd oneshot unit — writes sysfs early on Ubuntu/Debian without editing GRUB; good for config-management-driven hosts.
tuned— fits RHEL sites that already manage kernel tuning throughtuned-adm.
Step-by-step GRUB procedures:
Pick one persistence method—do not stack GRUB and systemd units that fight each other.
Troubleshooting
| Symptom | Likely cause | What to try |
|---|---|---|
AnonHugePages > 0 but enabled shows [never] |
Shared-memory THP or another size setting | Check shmem_enabled and hugepages-2048kB/enabled; see check THP guide |
| Latency spikes after kernel upgrade | defrag default or compaction behavior changed |
Compare defrag; try never on DB nodes |
| PostgreSQL will not use huge pages | THP on but no reserved pool | Turn THP off; reserve static pages per configure HugePages |
Opt-in mode but no AnonHugePages |
Mapping too small, not touched, or fragmented RAM | Touch pages; wait for khugepaged; read smaps_rollup |
| Redis warning at startup | enabled is always |
Set madvise or never per table above |
References
- Transparent Hugepage Support — Linux kernel documentation
- HugePages vs Transparent HugePages
- Check Transparent HugePages in Linux
- Linux memory management overview
- Disable THP on RHEL/CentOS 7
- Disable THP on RHEL 8 (GRUB2)
- Configure static HugePages (
vm.nr_hugepages)
Summary
You do not need to disable Transparent HugePages on every Linux box in 2026. Ubuntu and RHEL defaults (madvise) are a sensible middle ground: huge pages stay off until a program asks, or until policy allows merging.
Turn THP fully off (never on both enabled and defrag) when your database vendor says so, or when you measure fork slowdowns, long pauses during big allocations, or unwanted AnonHugePages on a dedicated Redis or DB server. On a general app server, keep madvise unless profiling shows a clear benefit from always.
On my test host, opting in to huge pages created ~400 MiB of AnonHugePages and roughly halved a sequential memory scan versus never, while defrag=always made the same allocation about 2.5× slower than defrag=madvise. THP is not a substitute for static HugePages—size vm.nr_hugepages separately when PostgreSQL or Kubernetes explicit huge pages are required.

