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::TimeoutError or ConnectionPool::TimeoutError
  • Redis::CannotConnectError: Timed out after 1.0s in application logs
  • Fragment caching silently fails, causing increased database load
  • Response times spike across all endpoints simultaneously
  • redis-cli info clients shows blocked_clients increasing

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. 1.Increase connection pool size for production:
  2. 2.```ruby
  3. 3.# config/environments/production.rb
  4. 4.config.cache_store = :redis_cache_store, {
  5. 5.url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" },
  6. 6.pool_size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i,
  7. 7.pool_timeout: 5,
  8. 8.connect_timeout: 2,
  9. 9.read_timeout: 2,
  10. 10.write_timeout: 2,
  11. 11.reconnect_attempts: 1,
  12. 12.error_handler: -> (method:, returning:, exception:) {
  13. 13.Rails.logger.error(
  14. 14."Redis error: #{exception.class} #{method} returning=#{returning}"
  15. 15.)
  16. 16.}
  17. 17.}
  18. 18.`
  19. 19.Match pool size to Puma configuration:
  20. 20.```ruby
  21. 21.# config/puma.rb
  22. 22.# Each Puma thread may need a Redis connection
  23. 23.max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
  24. 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. 1.Detect connection leaks:
  2. 2.```ruby
  3. 3.# lib/middleware/redis_pool_monitor.rb
  4. 4.class RedisPoolMonitor
  5. 5.def initialize(app)
  6. 6.@app = app
  7. 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. 1.Add health check for Redis connectivity:
  2. 2.```ruby
  3. 3.# app/controllers/health_controller.rb
  4. 4.class HealthController < ApplicationController
  5. 5.def redis
  6. 6.timeout = 2
  7. 7.result = Timeout.timeout(timeout) do
  8. 8.Rails.cache.redis.ping
  9. 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. 1.Configure Redis server for higher connection limits:
  2. 2.```bash
  3. 3.# Check current limits
  4. 4.redis-cli CONFIG GET maxclients
  5. 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