Introduction

Timeout::Error during external HTTP calls is a common production issue that can cascade into thread pool exhaustion and application-wide outages. When a downstream API becomes slow or unresponsive, every blocked thread holding an open connection can starve your web server of available workers.

Symptoms

  • Net::ReadTimeout or Faraday::TimeoutError in logs
  • Puma/Passenger worker threads maxed out waiting for responses
  • Cascading failures across multiple services
  • Response times spike from milliseconds to 30+ seconds
  • Connection pool exhausted: could not obtain a database connection

Example error: `` Net::ReadTimeout: Net::ReadTimeout with #<TCPSocket:(closed)> from /path/to/net/http.rb:1635:in rbuf_fill' from /path/to/net/http.rb:1612:in get' from /path/to/faraday-net_http-2.1.0/lib/faraday/adapter/net_http.rb:112:in request_with_wrapped_block' ```

Common Causes

  • Downstream API experiencing degraded performance or outage
  • No timeout configured on HTTP client (defaults to infinite)
  • Timeout set too high for production SLA requirements
  • DNS resolution delays adding to total request time
  • TLS handshake slow on cold connections

Step-by-Step Fix

  1. 1.Configure timeouts on Faraday client:
  2. 2.```ruby
  3. 3.Faraday.new(url: 'https://api.example.com') do |f|
  4. 4.f.request :retry, max: 2, interval: 0.05,
  5. 5.interval_randomness: 0.5, backoff_factor: 2,
  6. 6.exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
  7. 7.f.options.timeout = 5 # Open + read timeout in seconds
  8. 8.f.options.open_timeout = 3 # Connection establishment timeout
  9. 9.f.adapter :net_http
  10. 10.end
  11. 11.`
  12. 12.Configure timeouts on Net::HTTP directly:
  13. 13.```ruby
  14. 14.uri = URI('https://api.example.com/data')
  15. 15.http = Net::HTTP.new(uri.host, uri.port)
  16. 16.http.use_ssl = true
  17. 17.http.open_timeout = 3
  18. 18.http.read_timeout = 5
  19. 19.http.write_timeout = 5

response = http.get(uri.path) ```

  1. 1.Implement a circuit breaker pattern:
  2. 2.```ruby
  3. 3.require 'circuitbox'

class ExternalApi def self.call Circuitbox.circuit_breaker(:external_api, threshold: 5, # Failures before opening sleep_window: 30, # Seconds before trying again volume_threshold: 10 # Minimum requests before tripping ).run do http_client.get('/data') end end end ```

  1. 1.Add connection pooling to avoid TLS handshake overhead:
  2. 2.```ruby
  3. 3.require 'net-http-persistent'

class ApiClient def initialize @http = Net::HTTP::Persistent.new(name: 'api_client') @http.idle_timeout = 30 @http.max_requests_per_connection = 100 end

def get(path) uri = URI("https://api.example.com#{path}") @http.request(uri) end end ```

Prevention

  • Always set explicit timeouts on HTTP clients (never rely on defaults)
  • Use circuit breakers for all external service calls
  • Monitor downstream API response time percentiles (p50, p95, p99)
  • Implement request budgeting: total request time should be less than your SLA
  • Add connection pooling for high-frequency API calls
  • Use async HTTP calls (via async-http) for non-blocking operations