Introduction
Rails uses Redis as a cache store for fragment caching, Russian doll caching, and session storage. When the Redis connection pool is exhausted, requests block waiting for an available connection and eventually timeout. This creates cascading failures as queued requests consume application threads, eventually bringing down the entire application under load.
Symptoms
- Requests timeout with
Redis::TimeoutErrororConnectionPool::TimeoutError Redis::CannotConnectError: Timed out after 1.0sin application logs- Fragment caching silently fails, causing increased database load
- Response times spike across all endpoints simultaneously
redis-cli info clientsshowsblocked_clientsincreasing
Error in logs:
``
ConnectionPool::TimeoutError: Waited 1.0 sec, but connection pool is exhausted.
from connection_pool (2.4.1) lib/connection_pool.rb:110:in block in checkout'
from connection_pool (2.4.1) lib/connection_pool.rb:85:in checkout'
from redis-client (0.19.1) lib/redis_client/connection_pool.rb:27:in block in with'
```
Common Causes
- Connection pool size too small for Puma thread count
- Long-running requests holding connections too long
- Connection leak: connection not returned to pool after use
- Redis server maxclients limit reached
- Network latency between app and Redis causing slow checkout
Step-by-Step Fix
- 1.Increase connection pool size for production:
- 2.```ruby
- 3.# config/environments/production.rb
- 4.config.cache_store = :redis_cache_store, {
- 5.url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" },
- 6.pool_size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i,
- 7.pool_timeout: 5,
- 8.connect_timeout: 2,
- 9.read_timeout: 2,
- 10.write_timeout: 2,
- 11.reconnect_attempts: 1,
- 12.error_handler: -> (method:, returning:, exception:) {
- 13.Rails.logger.error(
- 14."Redis error: #{exception.class} #{method} returning=#{returning}"
- 15.)
- 16.}
- 17.}
- 18.
` - 19.Match pool size to Puma configuration:
- 20.```ruby
- 21.# config/puma.rb
- 22.# Each Puma thread may need a Redis connection
- 23.max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
- 24.min_threads_count = ENV.fetch("RAILS_MIN_THREADS", max_threads_count)
threads min_threads_count, max_threads_count
# Connection pool should match max threads # For multiple Redis consumers, multiply accordingly # 1 pool for cache + 1 for ActionCable + 1 for Sidekiq = 3x redis_pool_size = max_threads_count.to_i * 3 ENV["REDIS_POOL_SIZE"] = redis_pool_size.to_s ```
- 1.Detect connection leaks:
- 2.```ruby
- 3.# lib/middleware/redis_pool_monitor.rb
- 4.class RedisPoolMonitor
- 5.def initialize(app)
- 6.@app = app
- 7.end
def call(env) redis_client = Rails.cache.redis pool_info = redis_client._client.info rescue {}
before = { available: pool_info[:available_connections] || 0, size: pool_info[:pool_size] || 0, }
status, headers, body = @app.call(env)
after = redis_client._client.info rescue {} if after[:available_connections].to_i < before[:available].to_i Rails.logger.warn( "Redis connection not returned: #{env['REQUEST_PATH']} " \ "available: #{before[:available]} -> #{after[:available_connections]}" ) end
[status, headers, body] end end ```
- 1.Add health check for Redis connectivity:
- 2.```ruby
- 3.# app/controllers/health_controller.rb
- 4.class HealthController < ApplicationController
- 5.def redis
- 6.timeout = 2
- 7.result = Timeout.timeout(timeout) do
- 8.Rails.cache.redis.ping
- 9.end
if result == "PONG" render json: { status: "ok", redis: "connected" } else render json: { status: "error", redis: "no response" }, status: 503 end rescue Timeout::Error render json: { status: "error", redis: "timeout" }, status: 503 rescue => e render json: { status: "error", redis: e.message }, status: 503 end end ```
- 1.Configure Redis server for higher connection limits:
- 2.```bash
- 3.# Check current limits
- 4.redis-cli CONFIG GET maxclients
- 5.redis-cli INFO clients
# Increase max clients (in redis.conf) maxclients 10000 timeout 300 # Close idle connections after 5 minutes
# Restart Redis sudo systemctl restart redis ```
Prevention
- Set pool_size equal to RAILS_MAX_THREADS in all Redis configurations
- Monitor Redis connection pool metrics with Prometheus or Datadog
- Set reasonable timeouts (2-5 seconds) to prevent connection holding
- Use separate Redis instances for cache, sessions, and Sidekiq
- Add circuit breaker pattern for Redis failures to degrade gracefully
- Load test with realistic connection patterns before deploying