← Back to list

Why Rails App Memory Bloat Happens: Causes and Solutions (and How I Cut It by 20%)

Our Rails app’s memory usage kept climbing, and restarting workers became routine. At first, I suspected a memory leak in the application…

Katz Sakai · 2026-03-28 05:33 · 2 claps · 5.0 min read
#ruby-on-rails #ruby #performance #performance-tuning #linux
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🔓 · Open Source 🏔️ · Outdoor & Adventure

Why Rails App Memory Bloat Happens: Causes and Solutions (and How I Cut It by 20%)

Our Rails app’s memory usage kept climbing, and restarting workers became routine. At first, I suspected a memory leak in the application itself. But in this case, the bigger issue was glibc holding onto freed memory behind the scenes. In this post, I’ll explain why that happens and how setting a single environment variable reduced our production memory usage from 3 GB to 2.4 GB.

TL;DR: Key Takeaways

  • The reason a Rails app “appears to keep consuming memory” is that glibc, which Ruby relies on, retains freed memory internally for future reuse rather than returning it to the OS. This is different from a typical memory leak.
  • Starting with Ruby 3.3.0, you can optimize the heap by calling Process.warmup after the Rails application has finished booting. However, because this is meant to run at the end of application boot, it is not very useful for reducing memory usage in a long-running Rails app.
  • If you want a fix that does not require any product code changes, setting MALLOC_ARENA_MAX=2 as an environment variable is still effective. This limits the number of arenas glibc creates, encouraging it to reuse memory from existing pools instead of allocating new ones. As a result, freed memory is less likely to accumulate unnecessarily.
  • 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. 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]What causes Ruby memory bloat? Ruby apps can use a lot of memory. But why? Various people in the community attribute it to memory fragmentation, and…www.joyfulbikeshedding.com

According to that article, the reasons memory bloat “appears to occur” are as follows:

  • In our case, “heap page fragmentation on the Ruby side” was not a major cause of the higher memory usage.
  • 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.” See https://bugs.ruby-lang.org/issues/15667#note-10 for the detail.

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." See https://bugs.ruby-lang.org/issues/18885 for the detail.

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]Process.warmup: invoke malloc_trim if available by casperisfine · Pull Request #8451 · ruby/ruby Similar to releasing free GC pages, releasing free malloc pages reduce the amount of page faults post fork. NB: Some…github.com

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 suppress 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.

[embed]Malloc Can Double Multi-threaded Ruby Program Memory Usage Memory fragmentation is difficult to measure and diagnose, but it can also sometimes be very easy to fix. Let's look at…www.speedshop.co

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.


메타데이터
post_id
d288e71bf769
slug
why-rails-app-memory-bloat-happens-causes-and-solutions-2025-edition-d288e71bf769
url
https://medium.com/@ksakai/why-rails-app-memory-bloat-happens-causes-and-solutions-2025-edition-d288e71bf769
canonical_url
https://medium.com/@ksakai/why-rails-app-memory-bloat-happens-causes-and-solutions-2025-edition-d288e71bf769
author_url
https://medium.com/@ksakai
status
ok
fetched_at
2026-06-11 12:34:08