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: []) } end enum 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.value Queries 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 end create! 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 a free audit of your database.







