#Ruby on Rails
Releasing database connections after each web request in Ruby on Rails
January 28, 2025
//3 min read
Recently, I came across some discussion mentioning that Ruby on Rails checks out a database connection from the connection pool on the first use within a web request and checks it in once the request is complete. While this wasn’t new knowledge to me, I became curious about how it works under the hood.
A quick Google search and a discussion with ChatGPT pointed me to the ActiveRecord::ConnectionAdapters::ConnectionManagement
middleware. However, after inspecting the Rails source code on GitHub, I couldn’t find this middleware. Further research confirmed that it was used in older versions of Rails but is no longer present in newer releases.
After digging through the documentation and source code, I found the ConnectionPool
abstract class in activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb. Inside this class, I discovered the complete
method, which calls pool.release_connection
, responsible for checking in the active connection.
def complete(_)
ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
if (connection = pool.active_connection?)
transaction = connection.current_transaction
if transaction.closed? || !transaction.joinable?
pool.release_connection
end
end
end
end
This complete
method is hooked into an instance of ActiveSupport::Executor
within the Rails application whenever ActiveRecord is initialized.
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
def install_executor_hooks(executor = ActiveSupport::Executor)
executor.register_hook(ExecutorHooks)
end
# rails/activerecord/lib/active_record/railtie.rb
initializer "active_record.set_executor_hooks" do
ActiveRecord::QueryCache.install_executor_hooks
ActiveRecord::AsynchronousQueriesTracker.install_executor_hooks
ActiveRecord::ConnectionAdapters::ConnectionPool.install_executor_hooks
end
At this point, I understood that the connection is released back to the pool whenever the application’s executor completes.
The next step in my journey was figuring out when the executor is actually marked as complete. Further investigation into the Rails source code led me to the ActionDispatch::Executor
middleware. (I won’t dive into how Rack middleware works in Rails, as the official guides cover this extensively.) This middleware marks the executor’s state as complete once the web request is processed by the framework.
def call(env)
state = @executor.run!(reset: true)
begin
response = @app.call(env)
if env["action_dispatch.report_exception"]
error = env["action_dispatch.exception"]
@executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
end
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
rescue => error
...
ensure
state.complete! unless returned
end
end
This middleware is automatically included in the framework’s DefaultMiddlewareStack for Rails::Application
:
middleware.use ::ActionDispatch::Executor, app.executor
Recap
At this point, we know:
- The
ActionDispatch::Executor
middleware invokesActiveSupport::Executor.complete
. ActiveRecord
registers a callback forActiveSupport::Executor#complete
to release the active database connection back to the pool.
ActiveSupport Executor
The last piece of the puzzle was understanding the role of ActiveSupport::Executor
in this process.
I turned to the official guides again. As the Executor section in the “Threading and Code Execution in Rails” guide explains:
The Rails Executor separates application code from framework code: any time the framework invokes code you’ve written in your application, it will be wrapped by the Executor. The Executor consists of two callbacks:
to_run
andto_complete
. Theto_run
callback is triggered before application code runs, andto_complete
is called afterward.
Interestingly, this section also references the ActiveRecord::ConnectionAdapters::ConnectionManagement
middleware I encountered earlier:
Prior to Rails 5.0, some of these tasks were handled by separate Rack middleware classes (such as
ActiveRecord::ConnectionAdapters::ConnectionManagement
), or by directly wrapping code with methods likeActiveRecord::Base.connection_pool.with_connection
. The Executor replaces these with a more unified abstraction.
Essentially, application code is wrapped with an executor so that the Rails framework can perform necessary housekeeping tasks like tracking active threads, managing the ActiveRecord query cache, and—most importantly for our discussion—returning active connections back to the pool.
This fits perfectly with my understanding of the difference between a framework and a library: with a library, you call third-party code; with a framework, the framework calls your code. The executor is simply a mechanism that allows Rails to safely invoke your application code while managing its internal dependencies and phases under the hood.