Transparent Huge Pages can quietly add latency spikes to Redis, MongoDB and other databases. Check THP mode, defrag policy and real hugepage usage in one Ruby script.
Step through the build below:
Many database vendors tell you to disable or restrict Transparent Huge Pages because background compaction can stall allocations. But few teams actually check what their hosts run. This script reads /sys/kernel/mm/transparent_hugepage and /proc/meminfo, reports the active mode, and warns about risky combinations.
#!/usr/bin/env ruby
# frozen_string_literal: true
# thp_memory_audit.rb - report Transparent Huge Pages (THP) settings and usage,
# and flag configurations that commonly hurt databases (Redis, MongoDB, etc.).
# Usage: ruby thp_memory_audit.rb [--root /] (--root lets tests point at a fake tree)
require 'optparse'
root = '/'
OptionParser.new { |o| o.on('--root DIR', 'filesystem root (default /)') { |v| root = v } }.parse!
# The active value in these files is wrapped in brackets: "always [madvise] never"
def active_mode(path)
raw = File.read(path).strip
raw[/\[(\w+)\]/, 1] || raw
rescue Errno::ENOENT
nil
end
def meminfo(path)
File.readlines(path).each_with_object({}) do |l, h|
k, v = l.split(':', 2)
h[k] = v.to_i # kB for most fields; page counts for HugePages_*
end
end
thp = File.join(root, 'sys/kernel/mm/transparent_hugepage')
enabled = active_mode(File.join(thp, 'enabled'))
defrag = active_mode(File.join(thp, 'defrag'))
mi = meminfo(File.join(root, 'proc/meminfo'))
if enabled.nil?
puts 'THP not exposed by this kernel (no /sys/kernel/mm/transparent_hugepage).'
exit 0
end
anon_mb = mi.fetch('AnonHugePages', 0) / 1024.0
total_mb = mi.fetch('MemTotal', 1) / 1024.0
puts format('THP enabled : %s', enabled)
puts format('THP defrag : %s', defrag || 'n/a')
puts format('AnonHugePages: %.0f MB of %.0f MB (%.1f%%)', anon_mb, total_mb, anon_mb * 100 / total_mb)
puts format('Static HugePages: total=%d free=%d size=%d kB', mi.fetch('HugePages_Total', 0), mi.fetch('HugePages_Free', 0), mi.fetch('Hugepagesize', 0))
findings = []
findings << 'THP is "always": latency spikes from compaction are common on databases; prefer "madvise" or "never".' if enabled == 'always'
findings << 'defrag is "always": allocations can stall while the kernel compacts memory; prefer "madvise" or "defer+madvise".' if defrag == 'always'
if mi.fetch('HugePages_Total', 0).positive? && mi['HugePages_Free'] == mi['HugePages_Total']
findings << 'Static hugepages are reserved but completely unused: that memory is wasted.'
end
if findings.empty?
puts 'OK: no THP concerns found.'
else
findings.each { |f| puts "WARN: #{f}" }
exit 1
end
1. Read the sysfs files and extract the [bracketed] active value. 2. Parse meminfo into a hash. 3. Print THP mode, defrag, AnonHugePages and static hugepages. 4. Build a findings list (always-mode, always-defrag, unused reservation). 5. Exit 1 if anything is warned. A –root flag makes it testable with a fake tree.
THP enabled : always THP defrag : madvise AnonHugePages: 2048 MB of 16000 MB (12.8%) Static HugePages: total=64 free=64 size=2048 kB WARN: THP is "always": latency spikes from compaction are common on databases; prefer "madvise" or "never". WARN: Static hugepages are reserved but completely unused: that memory is wasted. exit 1 (expected 1) THP enabled : madvise THP defrag : madvise AnonHugePages: 0 MB of 16000 MB (0.0%) Static HugePages: total=0 free=0 size=0 kB OK: no THP concerns found. exit 0 (expected)
Full script + README on GitHub: ruby-devops-toolkit/thp-memory-audit
Know your hugepage settings
The kernel exposes THP configuration as text like always [madvise] never, where the bracketed word is active. Parsing that correctly is the whole trick, and it is easy in Ruby with a single regex.
The script also checks static hugepages: memory reserved with vm.nr_hugepages that nothing uses is simply wasted RAM. Together these two checks cover the most common hugepage misconfigurations.
- Linux kernel with THP support (most distributions)
- Ruby 2.7 or newer (tested on 3.3); standard library only
- No root needed; the files are world-readable
Complete code
#!/usr/bin/env ruby
# frozen_string_literal: true
# thp_memory_audit.rb - report Transparent Huge Pages (THP) settings and usage,
# and flag configurations that commonly hurt databases (Redis, MongoDB, etc.).
# Usage: ruby thp_memory_audit.rb [--root /] (--root lets tests point at a fake tree)
require 'optparse'
root = '/'
OptionParser.new { |o| o.on('--root DIR', 'filesystem root (default /)') { |v| root = v } }.parse!
# The active value in these files is wrapped in brackets: "always [madvise] never"
def active_mode(path)
raw = File.read(path).strip
raw[/\[(\w+)\]/, 1] || raw
rescue Errno::ENOENT
nil
end
def meminfo(path)
File.readlines(path).each_with_object({}) do |l, h|
k, v = l.split(':', 2)
h[k] = v.to_i # kB for most fields; page counts for HugePages_*
end
end
thp = File.join(root, 'sys/kernel/mm/transparent_hugepage')
enabled = active_mode(File.join(thp, 'enabled'))
defrag = active_mode(File.join(thp, 'defrag'))
mi = meminfo(File.join(root, 'proc/meminfo'))
if enabled.nil?
puts 'THP not exposed by this kernel (no /sys/kernel/mm/transparent_hugepage).'
exit 0
end
anon_mb = mi.fetch('AnonHugePages', 0) / 1024.0
total_mb = mi.fetch('MemTotal', 1) / 1024.0
puts format('THP enabled : %s', enabled)
puts format('THP defrag : %s', defrag || 'n/a')
puts format('AnonHugePages: %.0f MB of %.0f MB (%.1f%%)', anon_mb, total_mb, anon_mb * 100 / total_mb)
puts format('Static HugePages: total=%d free=%d size=%d kB', mi.fetch('HugePages_Total', 0), mi.fetch('HugePages_Free', 0), mi.fetch('Hugepagesize', 0))
findings = []
findings << 'THP is "always": latency spikes from compaction are common on databases; prefer "madvise" or "never".' if enabled == 'always'
findings << 'defrag is "always": allocations can stall while the kernel compacts memory; prefer "madvise" or "defer+madvise".' if defrag == 'always'
if mi.fetch('HugePages_Total', 0).positive? && mi['HugePages_Free'] == mi['HugePages_Total']
findings << 'Static hugepages are reserved but completely unused: that memory is wasted.'
end
if findings.empty?
puts 'OK: no THP concerns found.'
else
findings.each { |f| puts "WARN: #{f}" }
exit 1
end
Step by step
Extract the active mode
active_mode reads the file and uses raw[/\[(\w+)\]/, 1] to pull out the bracketed value, returning nil if the file does not exist (kernel without THP).
Parse /proc/meminfo
Each line is split on the first colon and converted with to_i, yielding kB values or page counts.
Report the facts
THP mode, defrag policy, AnonHugePages as a percent of RAM, and static hugepage totals are printed first.
Apply the rules
Mode always, defrag always, and fully free static hugepages each add a WARN line.
Make it testable
The --root option prefixes every path so the shell test can build a fake /sys and /proc tree.
What you should see
THP enabled : always
THP defrag : madvise
AnonHugePages: 2048 MB of 16000 MB (12.8%)
Static HugePages: total=64 free=64 size=2048 kB
WARN: THP is "always": latency spikes from compaction are common on databases; prefer "madvise" or "never".
WARN: Static hugepages are reserved but completely unused: that memory is wasted.
exit 1 (expected 1)
THP enabled : madvise
THP defrag : madvise
AnonHugePages: 0 MB of 16000 MB (0.0%)
Static HugePages: total=0 free=0 size=0 kB
OK: no THP concerns found.
exit 0 (expected)
- “THP not exposed”: containers or minimal kernels may hide sysfs; run on the host.
- Settings revert after reboot: persist via kernel cmdline
transparent_hugepage=madviseor a systemd unit. - “always” is not wrong for every workload; treat warnings as prompts to check your vendor guidance.
- Tested against a fake tree and the sandbox kernel.
- Compare against a per-role policy file (databases vs. batch hosts).
- Per-process THP use from
/proc/PID/smaps_rollup. - Emit Prometheus metrics for fleet dashboards.
- Fix mode automatically behind a –apply flag.