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.warmup` when 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=2` remains 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 jemalloc[^3]. 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.
{% embed https://www.joyfulbikeshedding.com/blog/2019-03-14-what-causes-ruby-memory-bloat.html %}
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."[^1]
## 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."[^2]
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.
{% embed https://github.com/ruby/ruby/pull/8451 %}
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](https://github.com/jemalloc/jemalloc), 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, setting `MALLOC_ARENA_MAX=2` usually 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.

[^1]: 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](https://bugs.ruby-lang.org/issues/15667#note-10)
[^2]: The background behind the introduction of `Process.warmup` is explained in [Feature #18885: End of boot advisory API for RubyVM - Ruby - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/18885)
[^3]: [Investing in Infrastructure: Meta’s Renewed Commitment to jemalloc](https://engineering.fb.com/2026/03/02/data-infrastructure/investing-in-infrastructure-metas-renewed-commitment-to-jemalloc/)