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!