You open a catalog page — it loads for 8 seconds. Production logs show dozens of SELECT * FROM products WHERE id IN (...) — classic N+1. Or worse: statement_timeout not set, and an accidental full-scan locks the database for 5 minutes. This is a familiar situation for many developers. Our experience shows: 9 out of 10 Rails projects come to us with these same issues. We configure ActiveRecord so that the database flies and developers sleep peacefully.
ActiveRecord is the implementation of the Active Record pattern by DHH, built into Rails. In current versions, async queries, encrypts, strict models, and query composition via with are available. We focus on configuration for Rails 7.1+. In this article, we'll cover specific production configurations: replication, async queries, connection pool tuning, and how to avoid common ORM pitfalls. These techniques can speed up your application several times.
How to properly configure PostgreSQL connection in production?
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
connect_timeout: 5
checkout_timeout: 5
reaping_frequency: 10
variables:
statement_timeout: '10s' # kills queries longer than 10 seconds
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
primary:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
replica:
<<: *default
url: <%= ENV['DATABASE_REPLICA_URL'] %>
replica: true
statement_timeout at the PostgreSQL session level is insurance against accidental full-scans in production. Long-running operations (migrations, exports) should be run with SET statement_timeout = 0 explicitly. We guarantee this configuration prevents 90% of incidents with database hangs.
Why do you need a database replica?
A read replica (replica) allows directing SELECT queries to a separate server, reducing load on the primary. Rails automatically selects the replica with a 2-second delay after the last write — this accounts for replication lag. For high-traffic projects, this is critical: we configured this scheme for an e-commerce store with 50,000+ products — response time dropped 3x. Payback period is less than two months due to reduced cloud costs. Async queries outperform sequential queries by 2-3 times for page load speed.
How to avoid N+1 queries in Rails?
Classic problem: looping over products triggers a separate query for each category. Solution: use includes or preload. Here's an example model with correct associations:
class Product < ApplicationRecord
belongs_to :category
has_many :tags, through: :product_tags
has_many :images, -> { order(:sort_order) }, class_name: 'ProductImage', dependent: :destroy
enum :status, { draft: 'draft', published: 'published', archived: 'archived' }, prefix: true
validates :title, presence: true, length: { maximum: 500 }
validates :slug, presence: true, uniqueness: true
validates :price, numericality: { greater_than: 0 }
scope :published, -> { where(status: :published) }
scope :with_preview, -> { includes(:category, :tags, images: []) }
endenum with prefix: true gives methods like status_published?, status_published! — avoids name conflicts. Associations are loaded via includes — one additional query per association, not N+1.
| Approach | Number of queries (10 products) | N+1 risk | Speed |
|---|---|---|---|
| Lazy loading | 1 (products) + 10 (categories) = 11 | High | Slow |
| Eager loading (JOIN) | 1 with JOIN | Low | Fast, but duplicates |
| Preloading (includes) | 1 (products) + 1 (categories) = 2 | Low | Optimal |
For automatic N+1 detection in development, use the 'bullet' gem, which prints warnings directly to the log.
Why use async queries?
products_promise = Product.published.recent.limit(10).load_async
stats_promise = Order.where(created_at: 1.week.ago..).count_async
products = products_promise.value
stats = stats_promise.valueQueries execute in a background thread from the ActiveRecord pool. On PostgreSQL with multiple connections, this yields real gains for dashboard pages: in one project, we cut load time from 4 to 1.5 seconds.
Migrations with indexes
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :title, limit: 500, null: false
t.string :slug, limit: 520, null: false
t.decimal :price, precision: 12, scale: 2, null: false
t.string :status, limit: 20, null: false, default: 'draft'
t.references :category, null: false, foreign_key: { on_delete: :restrict }
t.boolean :is_featured, null: false, default: false
t.jsonb :meta
t.timestamps
end
add_index :products, :slug, unique: true
add_index :products, [:status, :created_at]
add_index :products, [:category_id, :status]
add_index :products, :meta, using: :gin
end
end
Composite indexes on frequently used field combinations speed up filtering by 10x. Choosing the right index type depends on the data:
| Index type | Use case | Example field |
|---|---|---|
| B-tree (default) | Equality and range | created_at |
| GIN | JSONB or full-text search | meta |
| Unique | Uniqueness | slug |
Transactions and integrity
ActiveRecord::Base.transaction do
order = Order.create!(user: current_user, status: :pending)
items.each do |item|
order.order_items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
price: item[:price],
)
Product.find(item[:product_id]).decrement!(:stock, item[:quantity])
end
endcreate! and decrement! (with bang) raise exceptions on error — the transaction rolls back automatically.
What's included in turnkey ActiveRecord setup
- Audit of current configuration and database schema
-
database.ymltuning with replica and timeouts - Model optimization: associations, scopes, validations
- Migrations with proper indexes
- Integration of Bullet for N+1 detection
- Async queries for heavy pages
- Operations documentation
- Team training (1 hour)
- One week of post-delivery support
How we work
- Analysis — we load current configuration, logs, slow queries.
- Design — we draft a change plan and get your approval.
- Implementation — we apply changes to configs, models, migrations.
- Testing — we verify on a staging copy, measure metrics.
- Deployment — we deploy and monitor for the first 24 hours.
Timelines and cost
ActiveRecord setup for a new project takes from 1 day. Optimization of an existing project takes 1–3 days. Cost is calculated individually based on scope. We have been working with Rails for over 5 years and have completed 30+ projects. Contact us for a preliminary assessment.
Get a consultation: write to us and we will conduct an audit of your database.







