Rails applications can handle massive scale with proper optimization. Performance tuning involves multiple layers:
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/optimization-patterns.rbreferences/caching-guide.mdRails applications can handle massive scale with proper optimization. Performance tuning involves multiple layers:
Rails 8 provides excellent performance out of the box with Solid Cache, Thruster, and modern defaults.
The #1 performance problem in Rails apps:
Problem:
# 1 query for products + N queries for categories
products = Product.limit(10)
products.each { |p| puts p.category.name } # N additional queries!
Solution:
# 2 queries total
products = Product.includes(:category).limit(10)
products.each { |p| puts p.category.name } # No additional queries
Use the Bullet gem in development to detect N+1:
# Gemfile
gem 'bullet', group: :development
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.console = true
end
Add indexes for frequently queried columns:
# Migration
add_index :products, :sku
add_index :products, [:category_id, :available]
add_index :products, :name, unique: true
When to index:
Check with EXPLAIN:
Product.where(category_id: 5).explain
# Look for "Index Scan" (good) vs "Seq Scan" (bad)
# Select only needed columns
Product.select(:id, :name, :price)
# Use pluck for single values
Product.pluck(:name) # Returns array of names
# Count efficiently
Product.count # COUNT(*) query
Product.size # Smart: uses count or length based on context
# Check existence
Product.exists?(name: "Widget") # Fast
# Batch processing
Product.find_each { |p| process(p) } # Loads in batches
Rails 8 uses Solid Cache by default (database-backed).
Cache rendered view fragments:
<% cache @product do %>
<%= render @product %>
<% end %>
Cache key includes:
updated_at timestampCache multiple items efficiently:
<%= render partial: 'products/product', collection: @products, cached: true %>
Reads all caches in one query, much faster than individual caching.
Nest caches that invalidate properly:
class Product < ApplicationRecord
belongs_to :category, touch: true # Update category when product changes
end
<% cache @category do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
When product updates:
# Cache expensive calculations
def complex_stats
Rails.cache.fetch("product_#{id}/stats", expires_in: 1.hour) do
calculate_expensive_statistics
end
end
# Cache with dependencies
Rails.cache.fetch(["product", id, :reviews, last_review_at]) do
reviews.includes(:user).order(created_at: :desc).limit(10)
end
Rails automatically caches identical queries within a request:
Product.find(1) # Fires query
Product.find(1) # Uses cache (within same request)
Rails 8's asset pipeline:
Propshaft handles:
Thruster handles:
# config/environments/production.rb
config.asset_host = 'https://cdn.example.com'
Serves assets from CDN for faster global delivery.
# Use Active Storage variants
<%= image_tag @product.image.variant(resize_to_limit: [800, 600]) %>
# Or ImageProcessing gem
<%= image_tag @product.image.variant(resize_and_pad: [800, 600, background: "white"]) %>
# config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
Threads: Handle concurrent requests (5 is good default) Workers: Separate processes for parallelism (1 per CPU core)
Rules of thumb:
Rails 8 enables YJIT by default (Ruby 3.3+):
# config/application.rb
config.yjit = true # Enabled by default in Rails 8
YJIT benefits:
Use jemalloc for better memory management:
# Dockerfile
RUN apt-get install -y libjemalloc2
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
Reduces memory fragmentation with threaded servers.
# Gemfile
gem 'rack-mini-profiler'
Shows in-page:
Appears in top-left corner of every page in development.
# Gemfile
gem 'bullet', group: :development
# Detects:
# - N+1 queries (missing includes)
# - Unused eager loading
# - Unnecessary counter cache
Production performance monitoring:
includesfind_eachRails.cache.fetchrequire 'benchmark'
# Compare implementations
Benchmark.bm do |x|
x.report("approach 1:") { 1000.times { slow_method } }
x.report("approach 2:") { 1000.times { fast_method } }
end
# Apache Bench
ab -n 1000 -c 10 https://myapp.com/products
# wrk
wrk -t12 -c400 -d30s https://myapp.com/products
Production APM provides:
Causes:
Solutions:
includes)Causes:
Solutions:
memory_profiler gemderailed_benchmarksCauses:
Solutions:
includes for associationsFor deeper exploration:
references/caching-guide.md: Complete caching strategies guidereferences/profiling-tools.md: How to profile and debug performanceFor code examples:
examples/optimization-patterns.rb: Common optimization patternsRails performance involves:
Master these techniques and your Rails app will scale to millions of users.