bedda.tech logobedda.tech
← Back to blog

jemalloc + Huge Pages: 358MB RSS for 52MB Heap

Matthew J. Whitney
9 min read
infrastructurecloud computingdevopsbackend

The jemalloc transparent huge pages memory bloat problem is one of those infrastructure failures that looks like a memory leak until you stare at it long enough to realize the allocator and the kernel are conspiring against you in plain sight.

Picture this: your service is humming along. Live heap sits around 52MB, which is exactly what you'd expect given the workload. But your container keeps getting OOM-killed. You check RSS. It reads 358MB. You add more memory to the pod limit. It gets killed again three days later. You grep through your application code looking for unclosed buffers, forgotten caches, retained references. Nothing. The heap profile looks clean. The allocator stats look clean. And yet the kernel disagrees with all of it.

That scenario played out publicly this week when a post titled "jemalloc was using 358 MB RSS for a 52 MB live heap" hit the top of r/programming. The ratio is almost exactly 6.9x. That's not a rounding error. That's a systemic interaction between two systems that each work correctly in isolation and produce chaos together.

The Backend Mechanics: What's Actually Happening

To understand why RSS balloons this far beyond live heap, you need to understand what both jemalloc and the Linux kernel's Transparent Huge Pages (THP) subsystem are each trying to do.

jemalloc organizes memory into arenas, and within arenas into chunks (or "extents" in newer versions). When it requests memory from the OS via mmap, it typically requests memory in large aligned chunks so it can carve allocations out of them efficiently. This is the right design for a high-performance allocator. Less syscall overhead, better spatial locality, faster free-list management.

Linux THP, when set to always or even madvise mode with the wrong application behavior, promotes 4KB page groups to 2MB huge pages wherever it can. The kernel does this to reduce TLB pressure. Fewer entries needed for the same address range, faster page walks. Also a correct design goal.

The collision happens at the intersection. jemalloc requests a large aligned region. The kernel sees a large contiguous virtual range and promotes it to 2MB huge pages. Now when jemalloc carves a small allocation out of that region, the entire 2MB huge page containing that allocation is faulted into physical memory and counted toward RSS, even if jemalloc is only using a few kilobytes of it. The page cannot be split back into 4KB pages without the kernel's MADV_NOHUGEPAGE hint or an explicit madvise call.

So your 52MB of live heap is scattered across a much larger set of promoted huge pages. The huge pages are resident. The kernel counts them. Your RSS reads 358MB.

Infrastructure Diagnostics: Finding the Signal in the Noise

If you're debugging this in production, the first thing you need is to confirm THP is actually the culprit and not a real leak. These two look identical from the outside.

Check your kernel THP setting:

cat /sys/kernel/mm/transparent_hugepage/enabled

If it reads always, THP is applying to every mmap region that qualifies. If it reads [madvise], THP only applies when the application explicitly requests it via madvise(MADV_HUGEPAGE). jemalloc in some configurations does exactly that.

Then look at your process-level huge page stats:

grep -i hugepages /proc/<pid>/smaps_rollup

The AnonHugePages field tells you how much of your RSS is backed by 2MB pages. If that number is large relative to your live heap, you've confirmed the interaction.

jemalloc itself exposes stats through its mallctl interface. The key stats to pull are stats.resident, stats.active, and stats.allocated. If stats.allocated (live heap) is close to 52MB but stats.resident is approaching your observed RSS, jemalloc knows it's holding more physical pages than it's actively using. That gap is your huge page waste.

You can also use jeprof against a heap profile dump to visualize allocation patterns, but for this class of problem the smaps data is more revealing than the heap profile because the problem is about page granularity, not allocation site.

Cloud Computing Cost Implications

This matters more in containerized cloud environments than anywhere else, for a few compounding reasons.

Container memory limits are enforced against RSS, not against heap size. Your cgroup doesn't care that your allocator thinks it's only using 52MB. It sees 358MB resident and kills the process when the limit is exceeded. This produces OOM kills that look completely inexplicable from the application side.

In Kubernetes, this also interacts badly with the Vertical Pod Autoscaler if you're using it. VPA watches actual memory usage to set resource requests. If your pod consistently shows 350MB+ RSS due to THP bloat, VPA will recommend and apply larger memory requests. Your cluster gets more expensive. You provision more nodes. You pay more. And the underlying workload hasn't changed at all.

Multiply this across a fleet. If you're running 50 replicas of a service that should use 100MB each but is using 700MB due to jemalloc transparent huge pages memory bloat, you're provisioning 35GB of memory for a workload that needs 5GB. At current cloud pricing on any major provider, that's a meaningful monthly line item that shows up as "memory" in your cost allocation and gets attributed to application growth rather than an allocator/kernel interaction.

DevOps Mitigation: Kernel Tunables and Allocator Configuration

There are several levers available, and the right one depends on your environment.

Disable THP globally. The blunt instrument. Set:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

This eliminates the promotion entirely. You lose the TLB benefits for workloads that would genuinely benefit from huge pages (databases, certain ML inference workloads, large in-memory caches), but you stop the bloat. For most web services and API backends, TLB pressure from 4KB pages is not the bottleneck and this tradeoff is worth it.

Use madvise mode with careful application control. Setting THP to madvise means the kernel only promotes ranges that explicitly opt in. The problem is that jemalloc's behavior here is version and configuration dependent. jemalloc 5.x added the thp:0 option in its MALLOC_CONF environment variable to explicitly disable huge page advice:

MALLOC_CONF="thp:0" ./your-service

Check the jemalloc documentation for your specific version, since the exact option names shifted between major versions.

Use MADV_NOHUGEPAGE on jemalloc extents. jemalloc 5.2+ supports a hpa (Huge Page Allocator) configuration that gives you more explicit control over which extents get huge page treatment. If you're on a recent enough version, configuring hpa:false in MALLOC_CONF tells jemalloc to avoid requesting huge pages for its extents. This is more surgical than disabling THP globally.

Switch allocators. This is a real option, not a last resort. mimalloc from Microsoft Research handles THP interaction differently and tends to produce lower RSS in containerized workloads. tcmalloc has its own tradeoffs. If you're in a Rust service, the default system allocator on Linux often produces better RSS characteristics for certain allocation patterns than jemalloc does, though jemalloc still wins on throughput for allocation-heavy workloads.

Set vm.overcommit_memory and review your cgroup limits. This doesn't fix the bloat but it can prevent OOM kills while you diagnose and mitigate. Not a permanent solution.

The Deeper Pattern Worth Understanding

What makes jemalloc transparent huge pages memory bloat particularly tricky is that both systems are behaving correctly by their own design goals. jemalloc is being a good allocator. The kernel is being a good memory manager. The problem is emergent from their interaction, and emergent failures are the hardest to diagnose because no single component is broken.

Engineers who've spent time at the intersection of allocator internals and kernel memory management know this class of problem well. The TLB optimization that helps your database absolutely wrecks your allocator's RSS accounting. The arena-based chunk management that makes jemalloc fast for concurrent workloads creates exactly the large aligned regions that THP loves to promote.

The certstream.dev post that sparked the Reddit thread does a solid job walking through the specific numbers. The 6.9x ratio isn't random. It reflects how jemalloc's chunk sizes interact with 2MB huge page boundaries and the specific allocation pattern of that workload. Different workloads will produce different ratios, but the direction is always the same: RSS goes up, live heap stays flat, and the gap is entirely waste.

This is also a good reminder about latency and memory optimizations at the system level (a topic getting attention in the infrastructure community right now). System-level tunables like THP settings aren't set-and-forget. They interact with every allocator running on the host, and the right setting for a database host is often wrong for an API service host. In containerized environments where you don't control the host kernel settings, this creates a real operational gap: your application's RSS behavior is partly determined by a kernel setting you may not be able to change.

What to Do Right Now

If you're running jemalloc in a containerized backend service and you haven't checked your AnonHugePages to stats.allocated ratio, check it today. The diagnostic is fast. The fix, once confirmed, is a single environment variable in most cases.

If you're seeing OOM kills that don't match your heap profiles, add smaps_rollup inspection to your runbook. RSS lying to you is a known failure mode, and the kernel gives you the tools to understand why.

And if you're evaluating allocators for a new service, factor in RSS behavior under your actual allocation pattern, not just throughput benchmarks. Throughput wins on the benchmark. RSS wins in production billing and container stability. They're not the same thing.

The jemalloc project is actively maintained and the THP interaction is understood by the maintainers. Check the current release notes for your version before reaching for a global kernel tunable, since allocator-side fixes are often cleaner than host-wide configuration changes.

Have Questions or Need Help?

Our team is ready to assist you with your project needs.

Contact Us