Rails OOM error — diagnosis and fix
TL;DR
How to diagnose and fix Rails applications killed by the Linux OOM killer due to Ruby memory bloat.
Key facts
- Topic
- Production error triage
- Stack
- Ruby / Linux
TL;DR
When the Linux OOM killer terminates a Rails process, your application goes down without a graceful shutdown. Unlike a Ruby NoMemoryError (which is rare in practice), the OOM killer fires at the kernel level — the process is killed with SIGKILL, no rescue block runs, and in-flight requests are dropped. This is one of the most common Rails production failures, especially on memory-constrained VPS instances.
Confirm the OOM kill
Check kernel logs for the kill event:
dmesg | grep -i "killed process"
journalctl -k | grep -i "out of memory"
You will see output like:
Out of memory: Killed process 12345 (ruby) total-vm:2048000kB, anon-rss:1536000kB
Check which Puma workers consumed the most memory at the time:
ps aux --sort=-%rss | grep puma | head -10
smem -t -k -P puma
Why Rails processes bloat
Ruby's memory allocator (glibc malloc) is notoriously fragmentation-prone. Several patterns cause unbounded growth:
- Large ActiveRecord result sets —
Model.allorwherewithoutfind_eachloads the entire result set into Ruby objects. A table with 100k rows can consume hundreds of MB. - Memory fragmentation — glibc creates multiple memory arenas for multithreaded applications (Puma threads), and freed memory is rarely returned to the OS.
- String and Array churn — building large strings (CSV exports, XML generation) in a single buffer instead of streaming.
- Gem-level leaks — certain gems retain references in global state or class-level variables that grow per request.
Immediate fixes
Switch to jemalloc
jemalloc dramatically reduces Ruby memory fragmentation. Install and configure it:
sudo apt install -y libjemalloc2
Set the environment variable for your Puma service:
# In your systemd service or .env
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
Most Rails teams see a 20-40% reduction in memory usage after switching to jemalloc.
Limit glibc arenas
If you cannot switch to jemalloc, reduce glibc arena count:
MALLOC_ARENA_MAX=2
This limits the number of memory pools glibc creates, reducing fragmentation at the cost of slightly more lock contention.
Batch large queries
# Instead of loading everything at once
User.where(active: true).each { |u| process(u) }
# Use find_each to load in batches of 1000
User.where(active: true).find_each(batch_size: 1000) { |u| process(u) }
Configure Puma worker killer
The puma_worker_killer gem automatically restarts workers that exceed a memory threshold:
# config/puma.rb
before_fork do
require 'puma_worker_killer'
PumaWorkerKiller.config do |config|
config.ram = 1024 # MB total for all workers
config.frequency = 30 # check every 30 seconds
config.percent_usage = 0.90 # restart at 90% of ram limit
config.rolling_restart_frequency = 6 * 3600 # rolling restart every 6 hours
end
PumaWorkerKiller.start
end
Monitor memory trends
Track RSS over time to distinguish leaks (steady upward trend) from bloat (high but stable after warmup):
watch -n 5 'ps -o pid,rss,command -p $(pgrep -d, -f puma)'
Check system-wide memory pressure:
free -m
cat /proc/meminfo | grep -E "MemTotal|MemAvailable|SwapTotal|SwapFree"
Where Reflex helps
Reflex tracks per-worker RSS memory trends for your Puma processes. When memory usage crosses a configurable threshold, Reflex can trigger a phased restart of Puma workers — recycling them one at a time to preserve availability — before the OOM killer intervenes. It correlates memory growth with recent deployments and traffic patterns, giving your team actionable diagnostics. See How it works.