When running a Rails application in a production environment for a while, you may encounter a phenomenon where memory usage increases unexpectedly. In July 2025, I investigated the cause of this behavior and considered countermeasures, and this article summarizes my findings.
TL;DR: Key Takeaways
- The reason a Rails application "appears to be constantly consuming memory" is due to the design of glibc, which Ruby uses, which holds onto free memory internally instead of returning it to the OS for future reuse. This is not a typical memory leak.
- Since Ruby 3.3.0, it's possible to optimize the heap by calling
Process.warmupwhen a Rails application has finished booting. However, this mechanism is intended to be executed when the application has finished booting, making it difficult to use for reducing memory usage in Rails applications that are running for extended periods. - Setting the environment variable
MALLOC_ARENA_MAX=2remains an effective way to reduce memory usage without rewriting any product code. This setting prevents glibc from creating numerous arenas (memory pools) one after another, and instead reuses memory within existing memory pools, thus preventing glibc from accumulating too much free memory. -
Switching to jemalloc, which used to be a common recommendation, should now be avoided because jemalloc is no longer being maintained.Update Apr 3rd, 2026: Meta has recently announced a renewed commitment to jemalloc1. If that leads to active releases again, jemalloc may become an option again.
Why Does Memory Usage in Rails Apps Appear to Keep Growing?
Hongli Lai's article "What causes Ruby memory bloat?" covers this in detail.
According to that article, the reasons memory bloat "appears to occur" are as follows:
- The previously held belief that "heap page fragmentation on the Ruby side" was the primary cause of increased memory usage was not actually the main factor.
- The true cause was that "glibc's memory allocator, malloc, retains memory that Ruby has freed instead of returning it to the OS, holding onto it for future use." In particular, free pages that are not at the end of the heap are not returned to the OS, so unused memory continues to accumulate internally. From the OS's perspective, this makes it look like "Ruby keeps consuming memory."
- Calling glibc's
malloc_trim(0)ensures that memory freed by Ruby is returned to the OS, effectively reducing the process's memory usage (RSS) as seen by the OS.
Though there is one important caveat:
- Memory that Ruby has allocated and freed during processing is usually fragmented. Calling
malloc_trim(0)does not resolve the fragmentation; it merely returns the fragmented regions to the OS as-is. - Even if the memory is fragmented, it is still returned to the OS, so Ruby's memory usage (RSS) goes down. However, because other programs cannot allocate contiguous regions from fragmented free memory, an OOM (Out of Memory) error can occur even when there appears to be free memory available.
- Since returning fragmented memory to the OS does not make it easy to reuse effectively, malloc is designed to retain allocated but unused memory internally and reuse it, enabling stable allocation.
This is one of the reasons "Ruby has freed the memory, but malloc does not readily return it to the OS."2
What Memory-Related Improvement Was Added in Ruby 3.3.0?
Ruby 3.3.0 introduced the Process.warmup method. This method is intended to signal to the Ruby virtual machine from an application server that "the application's startup sequence has completed, making this an optimal time to perform GC and memory optimization."3
When Process.warmup is called, the Ruby virtual machine performs the following optimizations:
- Forces a major GC
- Compacts the heap
- Promotes all surviving objects to the old generation
- Pre-computes string coderanges (to speed up future string operations)
This cleans up objects and caches that were generated during application startup but are no longer needed, improving memory sharing efficiency in Copy-on-Write (CoW) environments.
Also, because unnecessary objects have already been collected and the heap has been compacted, malloc-side fragmentation is likely lower at this point. This makes it an ideal time to call malloc_trim(0), and a patch that calls malloc_trim(0) internally within Process.warmup has been merged.
Process.warmup: invoke `malloc_trim` if available
#8451
Similar to releasing free GC pages, releasing free malloc pages reduce the amount of page faults post fork.
NB: Some popular allocators such as jemalloc don't implement it, so it's a noop for them.
An important point is that Process.warmup is not automatically called behind the scenes like GC. It is the kind of method that should be explicitly called at an appropriate time on the application server side when a major GC would be acceptable (e.g., before forking, before worker startup). Therefore, there may not always be an appropriate time to call it in long-running Rails applications.
Reducing Memory Bloat in Long-Running Rails Apps
So how can you prevent memory bloat without using Process.warmup or malloc_trim(0)?
Online resources have recommended using jemalloc, a smarter memory allocator. However, jemalloc's repository was archived in June 2025, and it does not appear to be actively maintained. It is best to avoid adopting it for new projects. Update Apr 3rd, 2026: Meta has recently announced a renewed commitment to jemalloc. If that leads to active releases again, jemalloc may become an option again.
As an alternative, setting the environment variable MALLOC_ARENA_MAX=2 remains effective. Because:
- It reduces the number of arenas (memory management regions) that glibc allocates. glibc's malloc allocates numerous arenas as needed to prevent contention when multiple threads request memory simultaneously (normally, on 64-bit systems, the upper limit is 8 times the number of vCPU cores on the machine).
- As described above, glibc's memory allocator tends to hold on to memory instead of returning it to the OS. Therefore, the more arenas there are, the more "unreturned free memory" accumulates internally.
- Limiting the number of arenas can reduce the amount of memory glibc keeps, though it may slightly increase contention between threads during memory allocation.
The articles below suggest that MALLOC_ARENA_MAX=2 can cut memory usage noticeably, while increasing response time by only a few percent.
https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html
Additionally, MALLOC_ARENA_MAX=2 is the default setting on Heroku, which suggests it is a relatively safe configuration.
https://devcenter.heroku.com/changelog-items/1683
So why is MALLOC_ARENA_MAX=2 enough?:
- Ruby has a GVL (Global VM Lock), which means only one thread can execute Ruby code at any given time. So even if the application has many threads, only a small number of them are likely to be running and allocating memory at the same time. Consequently, glibc does not need to maintain many arenas; a small number (around
2) sufficient to handle requests from active threads should be adequate. For this reason, settingMALLOC_ARENA_MAX=2usually does not cause problems, while helping reduce the amount of freed memory glibc keeps across multiple arenas.
If you want to test it more carefully, compare memory usage and response time with the value unset, then with 2, 3, and 4, and see which works best for your app.
We compared the memory usage per Pod before and after setting MALLOC_ARENA_MAX=2. The solid line represents the usage after the setting was applied, and the dashed line represents the usage before. You can see the clear difference.
-
Investing in Infrastructure: Meta’s Renewed Commitment to jemalloc ↩
-
A proposal was made to "call
malloc_trim(0)when a full GC is performed in Ruby to return memory to the OS," but it was not implemented because returning fragmented memory to the OS provides little benefit since the OS cannot effectively utilize it. Feature #15667: Introduce malloc_trim(0) in full gc cycles - Ruby - Ruby Issue Tracking System ↩ -
The background behind the introduction of
Process.warmupis explained in Feature #18885: End of boot advisory API for RubyVM - Ruby - Ruby Issue Tracking System ↩

Top comments (0)