Rails Memory Optimization: Taming the OOM Killer in 2026

If you run a Ruby on Rails application at scale, you have likely received that dreaded 3:00 AM alert: R14 - Memory Quota Exceeded.

While Rails 8 and Ruby 3.5 have introduced massive performance gains, memory management remains a critical challenge. In fact, with the widespread adoption of YJIT (Yet Another Ruby JIT), swapping RAM for CPU speed has become a default trade-off, making memory constraints even tighter for smaller containers.

This guide covers how to optimize memory in the modern Rails ecosystem, from allocators to the specific quirks of the JIT compiler.

The Root Cause: Bloat vs. Leaks

Before diving into fixes, we must distinguish between two silent killers:

  1. Memory Leaks: A slow, continuous creep in RAM usage that never resets. (e.g., a global cache that grows indefinitely).
  2. Memory Bloat: A sudden spike caused by a specific action (like generating a PDF). Even after the request finishes, the Ruby process often holds onto that memory due to heap fragmentation.
The 2026 Context: In Ruby 3.5, the Garbage Collector is smarter, but fragmentation remains the #1 enemy of long-running web workers.

Phase 1: Infrastructure & The Runtime

The highest ROI changes in 2026 are often configuration-based rather than code-based.

1. jemalloc is Still King

Despite improvements in the default glibc allocator, jemalloc remains essential for Rails. It drastically reduces fragmentation.

The Impact: We typically see a 30% to 50% reduction in steady-state memory usage.

How to implement:

  • Heroku: Ensure you are using the heroku-buildpack-jemalloc.
  • Docker (Debian-based):
  • Dockerfile

# Install jemalloc
RUN apt-get update && apt-get install -y libjemalloc2
# Preload it
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2

2. Tuning YJIT Memory

YJIT is enabled by default in many Rails 8 setups for speed, but it consumes extra memory to store machine code. If you are on a memory-constrained environment (like a 512MB container), YJIT might cause OOM kills.

Strategy: If your app is crashing, try limiting the YJIT executable memory size:

Bash


# Limit YJIT to 64MB of RAM (Default can be significantly higher)
RUBY_YJIT_ENABLE=1
RUBY_YJIT_EXEC_MEM_SIZE=64

If you are severely memory-bound, you may need to disable YJIT (RUBY_YJIT_ENABLE=0) entirely, though you will sacrifice the Rails 8 speed boost.

Phase 2: Code-Level Optimization

Most memory bloat is self-inflicted by how we query ActiveRecord.

1. pluck vs. select

ActiveRecord objects in Rails 8 are lighter, but instantiating 10,000 of them is still heavy.

  • Bad (Allocates thousands of User objects):
  • Ruby

user_ids = User.where(active: true).map(&:id)
  • Good (Zero object instantiation):
  • Ruby

user_ids = User.where(active: true).pluck(:id)

2. Batching with find_each

Never load a whole table into an array. Use find_each to load records in batches (default 1000).

Ruby


# The memory-safe way to process millions of records
User.active.find_each do |user|
  # Rails 8 'Solid Queue' handles the backgrounding efficiently
  UserMailer.daily_digest(user).deliver_later
end

3. Frozen String Literals

In 2026, many codebases have moved to having this enabled by default via Rubocop or Ruby configuration, but explicit is better than implicit.

Ruby


# frozen_string_literal: true

This forces Ruby to reuse the same object for identical string literals, saving massive amounts of slots on the heap.

Phase 3: The Debugging Toolkit

1. Derailed Benchmarks

derailed_benchmarks is still the industry standard for analyzing boot-time memory.

Run this to see which gems are eating your RAM:

Bash


bundle exec derailed bundle:mem

2. Vernier (The Modern Profiler)

While stackprof and memory_profiler are classics, Vernier has become a favorite in 2025/2026 for its low-overhead visualization.

Ruby


Vernier.trace(out: 'memory_profile.json', hooks: [:memory]) do
  ReportsController.new.generate_heavy_csv
end

You can then upload the JSON to Firefox Profiler to visualize exactly where allocations happen over time.

Summary

Optimizing Rails memory in 2026 is about balancing the speed of YJIT with the efficiency of jemalloc.

Your Checklist:

  1. Is jemalloc enabled in production?
  2. Is YJIT configured correctly for your container size?
  3. Are you using batch processing for bulk data operations?