3 minutes
Mastering Multi‐DB and Horizontal Sharding in Rails
Introduction
As your Rails app grows, a single database can become a bottleneck for reads, writes, or geographic latency. Rails’ built‑in multi‑DB support and sharding features let you split load across replicas and shards while keeping code changes minimal. In this post, we’ll cover:
- Configuring primary/replica roles
- Automatic failover on lagging replicas
- Horizontal sharding patterns
- Handling migrations and monitoring
Configuring Primary and Replica
Rails 6.1 introduced role
support in config/database.yml
. Here’s a simple setup:
production:
primary:
adapter: postgresql
database: app_primary
host: primary.db.example.com
primary_replica:
adapter: postgresql
database: app_primary
host: replica.db.example.com
replica: true
Use connected_to
to route reads or writes:
# Read from replica
ActiveRecord::Base.connected_to(role: :reading) do
@users = User.where(active: true)
end
# Write to primary
ActiveRecord::Base.connected_to(role: :writing) do
User.create!(name: "New User")
end
You can also define default connection switching in controllers:
class ApplicationController < ActionController::Base
around_action :use_read_replica
private
def use_read_replica
ActiveRecord::Base.connected_to(role: :reading) { yield }
end
end
Automatic Failover Strategies
Replicas can lag behind the primary. Rails can detect this and fail back to the primary if lag is too high:
production:
primary_replica:
adapter: postgresql
database: app_primary
host: replica.db.example.com
replica: true
replica_lag_threshold: 5.seconds
If the replica’s pg_last_xact_replay_timestamp
is older than the threshold, Rails routes reads to the primary to avoid stale data. This ensures your users always see fresh content.
Horizontal Sharding Patterns
When you need to scale writes or distribute data geographically, sharding helps. Define shards in database.yml
:
production:
shards:
europe:
adapter: postgresql
database: app_europe
host: eu.db.example.com
asia:
adapter: postgresql
database: app_asia
host: asia.db.example.com
Switch shards in code:
# Using a simple selector
ShardSelector.with(:europe) do
Order.create!(region: 'EU', total: 100)
end
# Or use middleware to select based on request subdomain
For ActiveRecord models, you can also use connects_to shards:
:
class Order < ApplicationRecord
connects_to shards: {
europe: { writing: :europe },
asia: { writing: :asia }
}
end
Then:
Order.connected_to(shard: :asia) { Order.create!(...) }
Migrations and Maintenance
Migrations span multiple databases. Rails provides rake tasks:
rails db:migrate:primary # Migrate the primary database
rails db:migrate:shards # Migrate all shards
rails db:migrate:replicas # Migrate replicas if needed
Use ActiveRecord::Base.connection.handles
to inspect live connections:
ActiveRecord::Base.connection.handles.each do |role, conn|
puts "Role: #{role}, DB: #{conn.current_database}"
end
Monitor replication lag and shard health with tools like PgHero or custom SQL queries, and surface metrics in Grafana or Datadog.
Conclusion
Rails’ multi‑DB and sharding support give you the power to distribute reads, handle failover gracefully, and split your data across regions or services. With minimal code changes and built‑in rake tasks for migrations, you can scale your data layer as your app demands. Try these patterns to keep your Rails application resilient and performant under heavy load!