{"id":1548,"date":"2026-10-30T23:20:12","date_gmt":"2026-10-30T23:20:12","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=1548"},"modified":"2026-10-30T23:20:12","modified_gmt":"2026-10-30T23:20:12","slug":"tuning-kernel-thp-transparent-hugepage-latency-vps","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/","title":{"rendered":"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Transparent Huge Pages were supposed to be free performance: fewer TLB entries, fewer page faults, less kernel time per byte of memory touched. On a dedicated host with 256 GB of RAM and a settle-down workload they often are. On a 4 GB VPS running a pile of small processes, the same code path spends measurable CPU inside <code>khugepaged<\/code> scanning and compacting memory that will never form a 2 MB page anyway \u2014 and it does that work while holding locks your allocator wants.<\/p>\n\n<p class=\"wp-block-paragraph\">The result is a latency tail nobody attributes correctly. Median response time is unchanged, p99 grows by 20-40 ms in bursts every few seconds, and nothing in the application changed. This covers the three THP modes, what <code>khugepaged<\/code> actually costs, how to measure it from <code>\/sys\/kernel\/mm\/transparent_hugepage<\/code> and <code>\/proc\/vmstat<\/code>, and which mode fits which workload. Plan memory size decides whether THP helps at all, so check the <a href=\"https:\/\/virtualserversvps.com\/#providers\">provider comparison list<\/a> against your working set before tuning.<\/p>\n\n\n<h2 class=\"wp-block-heading\">The Three Modes and What Each Promises<\/h2>\n\n\n<p class=\"wp-block-paragraph\"><code>always<\/code> means the kernel attempts huge pages on every anonymous mapping, including the small heap of a 50 MB process. It also enables <code>khugepaged<\/code>, a background thread that scans for 512 adjacent 4 KiB pages to collapse into one 2 MB page, allocating and copying when it can. <code>madvise<\/code> disables the automatic attempt: only memory marked <code>MADV_HUGEPAGE<\/code> gets collapsed, and <code>khugepaged<\/code> only visits those ranges. <code>never<\/code> turns off both the fault-time attempt and background collapsing, leaving <code>hugetlbfs<\/code> as the only huge page mechanism. On a VPS, <code>never<\/code> is frequently right \u2014 not because huge pages are bad, but because collapse requires a copy, and the copy is where the tail latency lives.<\/p>\n\n\n<pre class=\"wp-block-code\"><code>cat \/sys\/kernel\/mm\/transparent_hugepage\/enabled\ncat \/sys\/kernel\/mm\/transparent_hugepage\/defrag\ncat \/sys\/kernel\/mm\/transparent_hugepage\/khugepaged\/pages_to_scan\ncat \/sys\/kernel\/mm\/transparent_hugepage\/khugepaged\/scan_sleep_millisecs<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">What khugepaged Costs a Small Guest<\/h2>\n\n\n<p class=\"wp-block-paragraph\"><code>khugepaged<\/code> wakes every 10 seconds, scans up to 4,096 pages for collapsible ranges, and for each candidate takes the range lock, allocates a 2 MB page, copies 512 pages into it, and remaps. The copy is the expensive part: 2 MB of memcpy per collapse plus page table work, under a lock that faults into the same range must wait on.<\/p>\n\n<p class=\"wp-block-paragraph\">On a 1 GB guest with a few PHP workers, almost nothing collapses \u2014 there are not 512 contiguous aligned pages in active use \u2014 but the scan still runs. On a 4 GB guest with a large heap the copies do happen, which is why the same default behaves well on one shape and badly on the other.<\/p>\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Setting<\/th><th>Default<\/th><th>Small VPS (1-4 GB)<\/th><th>Effect<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>enabled<\/td><td>always<\/td><td>madvise or never<\/td><td>removes background scanning<\/td><\/tr>\n<tr><td>defrag<\/td><td>madvise<\/td><td>never<\/td><td>no synchronous compaction stalls<\/td><\/tr>\n<tr><td>pages_to_scan<\/td><td>4096<\/td><td>1024<\/td><td>shorter scan bursts<\/td><\/tr>\n<tr><td>scan_sleep_millisecs<\/td><td>10000<\/td><td>30000<\/td><td>less frequent wakeups<\/td><\/tr>\n<tr><td>alloc_sleep_millisecs<\/td><td>60000<\/td><td>60000<\/td><td>unchanged<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Compaction Stalls Are the Real Problem<\/h2>\n\n\n<p class=\"wp-block-paragraph\">With <code>defrag<\/code> set to <code>always<\/code>, an allocation that cannot be satisfied from the free lists triggers direct compaction in the allocating thread. That thread walks memory, migrates pages, and blocks until it either succeeds or gives up after a retry limit. The stall is not the migration itself but the page lock contention it creates for every process touching the pages being moved.<\/p>\n\n<p class=\"wp-block-paragraph\">This is why <code>always<\/code> plus <code>defrag=always<\/code> is the worst combination on a small guest: it combines background scanning with synchronous stalls in the fault path. If you must use <code>always<\/code>, set <code>defrag<\/code> to <code>madvise<\/code> at minimum, and accept that the fault path will simply fall back to 4 KiB pages instead of blocking.<\/p>\n\n\n<h2 class=\"wp-block-heading\">Measuring It Instead of Guessing<\/h2>\n\n\n<p class=\"wp-block-paragraph\">The counters you need are all in <code>\/proc\/vmstat<\/code>. <code>thp_fault_alloc<\/code> counts fault-time allocations, <code>thp_collapse_alloc<\/code> counts background collapses, and <code>thp_split_page<\/code> counts pages broken apart again. Two splits per collapse means the collapsing is wasted work. Compare over the same 10-minute window as your latency data.<\/p>\n\n\n<pre class=\"wp-block-code\"><code>grep -E 'thp_(fault_alloc|collapse_alloc|split_page|zero)' \/proc\/vmstat\ngrep -E 'compact_(stall|success|fail)' \/proc\/vmstat\n\n# stall time in ms is the last field of \/proc\/vmstat's compact_stall,\n# pair it with latency: run both for 10 minutes, then compare\nawk '\/thp_collapse_alloc\/ {print \"collapses\", $2}' \/proc\/vmstat\nawk '\/thp_split_page\/ {print \"splits\", $2}' \/proc\/vmstat\n\n# per-process view of anonymous huge pages\ngrep -A 3 AnonHugePages \/proc\/meminfo\nfor p in \/proc\/[0-9]*\/smaps; do grep -l 'AnonHugePages: *[1-9]' $p; done 2&gt;\/dev\/null | head<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">A clean signature of the problem: <code>thp_collapse_alloc<\/code> climbing steadily while <code>thp_split_page<\/code> climbs at a similar rate, and <code>compact_stall<\/code> flat. That means background collapsing is churning without producing a stable benefit \u2014 the pages are formed and then unmapped or split before they pay back their copy cost.<\/p>\n\n\n<h2 class=\"wp-block-heading\">How Redis, Postgres, and the JVM Respond<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Redis is the clearest case for leaving THP off. The allocator uses <code>fork()<\/code> for background saves, and copy-on-write over 2 MB pages means a child that writes one key dirty an entire 2 MB region. RSS after the fork doubles, and a save that should touch 10 MB of dirty pages can touch hundreds. The Redis documentation has said this for years; it is not an opinion.<\/p>\n\n<p class=\"wp-block-paragraph\">PostgreSQL benefits in the shared buffer pool, which is contiguous and long-lived, so <code>madvise<\/code> plus setting <code>huge_pages = try<\/code> gets you large pages where it matters. The JVM is the one case where <code>always<\/code> genuinely helps a large heap, because a 4 GB heap has hundreds of thousands of TLB entries&#8217; worth of references; below 1 GB heap, the win is noise.<\/p>\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Workload<\/th><th>Mode<\/th><th>Reason<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Redis \/ Valkey<\/td><td>never<\/td><td>fork COW amplification<\/td><\/tr>\n<tr><td>Postgres, pool &gt; 1 GB<\/td><td>madvise<\/td><td>contiguous shmem, no background churn<\/td><\/tr>\n<tr><td>JVM heap &gt; 2 GB<\/td><td>always, defrag=madvise<\/td><td>TLB reach on a large live set<\/td><\/tr>\n<tr><td>JVM heap &lt; 1 GB<\/td><td>madvise<\/td><td>no measurable win<\/td><\/tr>\n<tr><td>PHP-FPM \/ Node workers<\/td><td>never<\/td><td>short-lived, small allocations<\/td><\/tr>\n<tr><td>Large in-memory cache, pinned<\/td><td>madvise<\/td><td>collapse once, never split<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Making the Change Without a Reboot<\/h2>\n\n\n<p class=\"wp-block-paragraph\"><code>enabled<\/code> and <code>defrag<\/code> are writable at runtime, and the change affects new mappings only \u2014 existing pages keep their current size until they are unmapped. For Redis, set THP to <code>never<\/code> and then restart Redis so its heap is faulted in fresh. Setting it and leaving the process running accomplishes very little, which is why people conclude the setting does not matter.<\/p>\n\n\n<pre class=\"wp-block-code\"><code>echo madvise &gt; \/sys\/kernel\/mm\/transparent_hugepage\/enabled\necho madvise &gt; \/sys\/kernel\/mm\/transparent_hugepage\/defrag\n# persist without systemd (VPS without unit-file control)\nprintf '#!\/bin\/sh\\necho madvise &gt; \/sys\/kernel\/mm\/transparent_hugepage\/enabled\\n' \\\n  &gt; \/etc\/rc.local &amp;&amp; chmod +x \/etc\/rc.local<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">A Test Protocol That Produces a Number<\/h2>\n\n\n<p class=\"wp-block-paragraph\">Fix the workload, run it for ten minutes under each mode, and record p50, p99, and p99.9 from the application&#8217;s own timer \u2014 not a proxy, which adds its own queueing. Sample <code>\/proc\/vmstat<\/code> deltas alongside. Three runs per mode, because THP effects are bursty and one run lands on either side of the noise.<\/p>\n\n<p class=\"wp-block-paragraph\">Expect the difference only in the tail. If p99 moves and p50 does not, the mechanism is compaction or <code>khugepaged<\/code> contention, and <code>madvise<\/code> will fix it. If both move, you changed something else too \u2014 usually because the restart emptied a cache that had nothing to do with page size. Run it on real memory pressure; the <a href=\"https:\/\/virtualserversvps.com\/#features\">VPS feature breakdown<\/a> shows the RAM each tier delivers, and THP choices only matter where the working set is large.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Transparent Huge Pages were supposed to be free performance: fewer TLB entries, fewer page faults, less kernel time per byte of memory touched. On a dedicated host with 256 GB&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":0,"footnotes":""},"categories":[3],"tags":[],"class_list":["post-1548","post","type-post","status-publish","format-standard","hentry","category-performance-optimization"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off - Virtual Servers VPS Blog<\/title>\n<meta name=\"description\" content=\"Transparent Huge Pages were supposed to be free performance: fewer TLB entries, fewer page faults, less kernel time per byte of memory touched. On a\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off\" \/>\n<meta property=\"og:description\" content=\"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-30T23:20:12+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/\",\"name\":\"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-10-30T23:20:12+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"description\":\"Transparent Huge Pages were supposed to be free performance: fewer TLB entries, fewer page faults, less kernel time per byte of memory touched. On a\",\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off - Virtual Servers VPS Blog","description":"Transparent Huge Pages were supposed to be free performance: fewer TLB entries, fewer page faults, less kernel time per byte of memory touched. On a","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/","og_locale":"en_US","og_type":"article","og_title":"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off","og_description":"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off","og_url":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-10-30T23:20:12+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/","url":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/","name":"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-10-30T23:20:12+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"description":"Transparent Huge Pages were supposed to be free performance: fewer TLB entries, fewer page faults, less kernel time per byte of memory touched. On a","breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/tuning-kernel-thp-transparent-hugepage-latency-vps\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Transparent Huge Pages on a VPS: When madvise Beats always and Neither Beats Off"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1548","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=1548"}],"version-history":[{"count":1,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1548\/revisions"}],"predecessor-version":[{"id":1551,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/1548\/revisions\/1551"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=1548"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=1548"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=1548"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}