Active Record Patterns That Keep Rails Apps Fast
Rails makes database access feel effortless—until it doesn't. A page that worked fine with 100 users slows down at 10,000 because of N+1 queries, missing indexes, or callbacks doing too much work. The good news: most Rails performance wins come from a handful of repeatable patterns.
Kill N+1 Queries First
The classic mistake: loading a list of records, then hitting the database again for each associated record.
# Bad — one query per project
@projects = current_user.projects
@projects.each { |p| puts p.client.name }
# Good — two queries total
@projects = current_user.projects.includes(:client)
Use includes for most cases. Reach for preload when you don't need to filter on the association, or eager_load when you do:
Project.includes(:client).where(clients: { active: true })
Add the bullet gem in development to catch N+1s before they reach production.
Index What You Query
Active Record can't fix a missing index. If your scopes filter or sort on a column, index it:
add_index :orders, :user_id
add_index :orders, [:status, :created_at]
add_index :products, :slug, unique: true
Review slow query logs regularly. EXPLAIN ANALYZE on PostgreSQL tells you exactly what the planner is doing.
Avoid .count in Loops
# Bad
users.each { |u| puts u.posts.count }
# Good — load counts in one query
users_with_counts = User.left_joins(:posts)
.group(:id)
.select("users.*, COUNT(posts.id) AS posts_count")
For simple cases, counter_cache on the association is even cleaner:
# migration
add_column :users, :posts_count, :integer, default: 0, null: false
# model
belongs_to :user, counter_cache: true
Use find_each for Batch Processing
Never iterate millions of rows with .each:
# Bad — loads everything into memory
User.where(inactive: true).each { |u| u.archive! }
# Good — batches of 1000
User.where(inactive: true).find_each(batch_size: 1000) { |u| u.archive! }
For heavy backfills, use background jobs and idempotent tasks so failures can retry safely.
Keep Callbacks Thin
after_save callbacks that send emails, sync search indexes, and charge cards turn models into god objects. Prefer explicit service objects or jobs:
# app/services/orders/complete.rb
class Orders::Complete
def call(order)
order.transaction do
order.update!(status: :completed)
OrderMailer.receipt(order).deliver_later
SearchIndexJob.perform_later(order.id)
end
end
end
Callbacks for simple normalization (before_validation :strip_whitespace) are fine. Side effects belong elsewhere.
Select Only What You Need
Large text columns and JSON blobs add up:
# Bad — pulls every column including huge :body fields
Post.published.limit(50)
# Good — list view only needs metadata
Post.published.select(:id, :title, :slug, :published_at).limit(50)
Use dedicated serializers or as_json(only: [...]) for APIs with strict payload size requirements.
Cache Expensive Reads
Fragment caching works well with Russian doll caching for nested partials:
<% cache project do %>
<%= render project %>
<% end %>
For aggregate stats that change infrequently, use Rails.cache.fetch:
Rails.cache.fetch("dashboard/stats", expires_in: 5.minutes) do
{ users: User.count, revenue: Order.paid.sum(:total) }
end
Rails 8's Solid Cache makes Redis optional for many apps.
Background Jobs for Slow Work
Anything over ~200ms belongs off the request cycle:
- PDF generation
- Third-party API calls
- Bulk imports
- Webhook delivery
Solid Queue (Rails 8 default) or Sidekiq keeps users seeing fast responses while work happens asynchronously.
Monitor Before You Optimize
Tools worth adding early:
- Scout, Skylight, or AppSignal — Request-level profiling
- PgHero — Index and query insights for PostgreSQL
- Lograge — Structured request logs
Measure first. Optimizing the wrong layer wastes time.
When to Worry About Scale
Most Rails apps never need sharding or read replicas. Fix queries, add indexes, cache hot paths, and move slow work to jobs. That covers the vast majority of production slowdowns.
If you're outgrowing a single database, read replicas and connection pooling (PgBouncer) come before rewriting in another language.
Wrapping Up
Rails performance isn't magic—it's disciplined database access, thin models, and async work where it belongs. Nail these patterns early and your app stays fast as traffic grows.
Running a slow Rails app or planning a new build? Contact NekoCoding for a performance review or architecture consult.