Rails 7.2 Connection Pool Changes May Slow Down Your App

After upgrading to Rails 7.2, you might notice your app is a bit slower. Not dramatically, but measurably—around 5-6% in real-world benchmarks. It comes down to how ActiveRecord now manages database connections.

The Change

In Rails 7.1, each thread held onto its database connection for the duration of a request.

Rails 7.2 flipped this: connections are now checked out for each query and returned immediately after. The goal was better connection sharing in multi-threaded environments where connections are scarce.

The change was introduced in PR #50793, replacing ActiveRecord::Base.connection with ActiveRecord::Base.with_connection for temporary access and lease_connection for longer holds.

Why This Causes Slowdowns

The check-in/check-out cycle isn’t free. Every time a connection returns to the pool, checkin callbacks run and the callback system allocates objects. A request with 10 queries now does 10 check-ins instead of zero.

Benchmarks from issue #55728 show the impact:

Rails 7.1:              98.55 req/sec
Rails 7.2:              92.45 req/sec  (6% slower)
Rails 7.2 + workaround: 99.99 req/sec  (back to normal)

You’ll feel this most if you’re running single-threaded workers like Unicorn, have a local or low-latency database, and execute many queries per request.

The Fix

For single-threaded workers where connection sharing doesn’t matter, lease a connection at the start of each request:

class ApplicationController < ActionController::Base
  before_action :lease_database_connection

  private

  def lease_database_connection
    ActiveRecord::Base.lease_connection
  end
end

This holds the connection for the entire request, skipping the check-in/check-out dance.

For background jobs, same idea:

class ApplicationJob < ActiveJob::Base
  before_perform do
    ActiveRecord::Base.lease_connection
  end
end

When to Skip This

If you’re running multi-threaded Puma with more threads than database connections, the new behavior is actually what you want—it lets threads share connections. Same goes for long-running requests that don’t touch the database much. Holding connections you don’t need starves other threads.

Under the Hood

Most of the overhead is in run_callbacks(:checkin). Even with no custom callbacks, Rails runs the full callback machinery—object allocations, method dispatch, the works.

Jean Boussier (byroot) has been working on optimizations for Rails main, but these won’t be backported to 7.2 since they’re performance tweaks rather than bug fixes.

Measuring Your App

Before changing anything, check if this even affects you:

# config/initializers/connection_benchmark.rb
if Rails.env.development?
  ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    Rails.logger.debug "SQL (#{event.duration.round(2)}ms)"
  end
end

Compare request times with and without lease_connection. If you don’t see a difference, don’t bother with the fix.

For Unicorn or single-threaded Puma deployments seeing slower responses after upgrading, give lease_connection a try. It’s safe and brings you back to 7.1 behavior.

Follow issue #55728 if you want to track the ongoing optimization work.