Rails with_connection: The better way to manage database connections
Rails applications often struggle with database connection management in high-concurrency environments. The traditional ActiveRecord::Base.connection
method holds connections until the end of the request cycle, potentially exhausting the connection pool.
Since Rails 7.2, there’s a better way: ActiveRecord::Base.with_connection
.
Before
Previously, when performing database operations, connections were held for the entire request duration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class DataImportService
def import_large_dataset
# This holds a connection for the entire method execution
connection = ActiveRecord::Base.connection
csv_data.each_slice(1000) do |batch|
# Long-running operations holding the connection
process_batch(batch, connection)
# External API calls still holding the connection
notify_external_service(batch)
end
end
private
def process_batch(batch, connection)
connection.execute("INSERT INTO imports ...")
end
end
This approach leads to:
- Connection pool exhaustion in high-traffic scenarios
- Reduced application throughput
- Database connection timeouts
- Poor resource utilization during I/O operations
The Better Approach
Rails 7.2 introduced ActiveRecord::Base.with_connection
which automatically manages connection lifecycle:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DataImportService
def import_large_dataset
csv_data.each_slice(1000) do |batch|
# Connection is only held during database operations
ActiveRecord::Base.with_connection do |connection|
process_batch(batch, connection)
end
# Connection is released back to pool during API calls
notify_external_service(batch)
end
end
private
def process_batch(batch, connection)
connection.execute("INSERT INTO imports ...")
end
end
The connection is automatically:
- Obtained from the pool when the block starts
- Yielded to the block for database operations
- Returned to the pool when the block completes
Real-world Example: Parallel Processing
Here’s how with_connection
shines in concurrent scenarios:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BulkUserUpdater
def update_users_in_parallel
User.find_in_batches(batch_size: 100) do |user_batch|
# Process batches in parallel threads
user_batch.map do |user|
Thread.new do
# Each thread gets its own connection from the pool
ActiveRecord::Base.with_connection do |connection|
# Perform complex calculations
analytics_data = fetch_user_analytics(user)
# Update user with connection
connection.execute(<<-SQL)
UPDATE users
SET last_activity = '#{analytics_data[:last_activity]}',
engagement_score = #{analytics_data[:score]}
WHERE id = #{user.id}
SQL
end
# Connection released while sending emails
UserMailer.activity_summary(user).deliver_later
end
end.each(&:join)
end
end
end
The Soft Deprecation
While ActiveRecord::Base.connection
has been soft deprecated since Rails 7.2, it still works without warnings by default. To see deprecation warnings, you need to explicitly configure:
1
2
3
4
5
6
7
# config/application.rb
ActiveRecord.permanent_connection_checkout = :deprecated
# Now you'll see:
ActiveRecord::Base.connection
# DEPRECATION WARNING: ActiveRecord::Base.connection is deprecated.
# Use #lease_connection instead.
The method has been renamed to lease_connection
to better reflect that it holds the connection for the entire request duration.
When to Use lease_connection
For cases requiring manual connection management, use lease_connection
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LongRunningJob
def perform
# Manually lease a connection
connection = ActiveRecord::Base.lease_connection
begin
# Use connection for multiple operations
connection.transaction do
update_records(connection)
generate_reports(connection)
end
ensure
# Must manually release the connection
ActiveRecord::Base.connection_handler.clear_active_connections!
end
end
end
Use lease_connection
when you need:
- Explicit control over connection lifecycle
- A connection across multiple method calls
- Custom connection handling logic
Performance Benefits
The new approach provides significant improvements:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Benchmark comparing old vs new approach
require 'benchmark'
Benchmark.bm do |x|
x.report("old approach:") do
100.times do
conn = ActiveRecord::Base.connection
conn.execute("SELECT COUNT(*) FROM users")
sleep(0.01) # Simulate I/O operation
end
end
x.report("with_connection:") do
100.times do
ActiveRecord::Base.with_connection do |conn|
conn.execute("SELECT COUNT(*) FROM users")
end
sleep(0.01) # Connection released during sleep
end
end
end
# user system total real
# old approach: 0.024548 0.024413 0.048961 ( 1.511756)
# with_connection: 0.010594 0.005119 0.015713 ( 1.311284)
The results show:
- 15% faster real time (1.31s vs 1.51s)
- 68% less total CPU time (0.015s vs 0.048s)
- Better connection pool utilization during I/O operations
Migration Guide
Update your existing code patterns:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Before
def execute_query
conn = ActiveRecord::Base.connection
conn.execute("SELECT * FROM products")
end
# After
def execute_query
ActiveRecord::Base.with_connection do |conn|
conn.execute("SELECT * FROM products")
end
end
# Or for simple cases, just use ActiveRecord methods
def execute_query
Product.all
end
Conclusion
The new with_connection
method solves a real problem - connection pool exhaustion during I/O heavy operations. It’s a simple change that makes Rails applications handle concurrent requests better without any extra configuration.
References
- Pull Request #51083 introducing
with_connection
- Pull Request #51230 deprecating
connection
method - Rails Connection Handling Documentation
- Connection Pool Management Guide