Skip to content

nmagedman/the-ultimate-guide-to-ruby-timeouts

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The Ultimate Guide to Ruby Timeouts

An unresponsive service is worse than a down one. It can tie up your entire system if not handled properly. All network requests should have a timeout.

Here’s how to add timeouts for popular Ruby gems. All have been tested. You should avoid Ruby’s Timeout module. The default is no timeout, unless otherwise specified. Enjoy!

Common Types

  • connect (or open) - time to open the connection
  • read (or receive) - time to receive data after connected
  • write (or send) - time to send data after connected
  • checkout - time to checkout a connection from the pool
  • statement - time to execute a database statement

Gems

Data Stores

HTTP Clients

Web Servers

Rack Middleware

External Services

Bonus

Data Stores

activerecord

  • postgres adapter

    ActiveRecord::Base.establish_connection(connect_timeout: 1, checkout_timeout: 1, ...)

    or in config/database.yml

    production:
      connect_timeout: 1
      checkout_timeout: 1

    Raises

    • PG::ConnectionBad on connect and read timeouts
    • ActiveRecord::ConnectionTimeoutError on checkout timeout

    See also PostgreSQL statement timeouts

  • mysql2 adapter

    ActiveRecord::Base.establish_connection(connect_timeout: 1, read_timeout: 1, write_timeout: 1, checkout_timeout: 1, ...)

    or in config/database.yml

    production:
      connect_timeout: 1
      read_timeout: 1
      write_timeout: 1
      checkout_timeout: 1

    Raises

    • Mysql2::Error on connect and read timeouts
    • ActiveRecord::ConnectionTimeoutError on checkout timeout

bunny

Bunny.new(connection_timeout: 1, ...)

Raises Bunny::TCPConnectionFailedForAllHosts on connect timeout

TODO read timeout

connection_pool

ConnectionPool.new(timeout: 1) { ... }

Raises Timeout::Error

dalli

Dalli::Client.new(host, socket_timeout: 1, ...)

Default: 0.5s

Raises Dalli::RingError

elasticsearch

Elasticsearch::Client.new(transport_options: {request: {timeout: 1}}, ...)

Raises

  • Faraday::ConnectionFailed on connect timeout
  • Faraday::TimeoutError on read timeout

mongo

Mongo::Client.new([host], connect_timeout: 1, socket_timeout: 1, server_selection_timeout: 1, ...)

Raises Mongo::Error::NoServerAvailable on connect timeout

TODO read timeout

mongoid

production:
  clients:
    default:
      options:
        connect_timeout: 1
        socket_timeout: 1
        server_selection_timeout: 1

Raises Mongo::Error::NoServerAvailable on connect timeout

TODO read timeout

mysql2

Mysql2::Client.new(connect_timeout: 1, read_timeout: 1, write_timeout: 1, ...)

Raises Mysql2::Error

pg

PG.connect(connect_timeout: 1, ...)

Raises PG::ConnectionBad

redis

Redis.new(connect_timeout: 1, timeout: 1, ...)

Raises

  • Redis::CannotConnectError on connect timeout
  • Redis::TimeoutError on read timeout

searchkick

Searchkick.timeout = 1

Default: 10s

Raises same exceptions as elasticsearch

sequel

  • postgres adapter

    Sequel.connect(connect_timeout: 1, pool_timeout: 1, ...)
    • Sequel::DatabaseConnectionError on connect and read timeouts
    • Sequel::PoolTimeout on checkout timeout
  • mysql2 adapter

    Sequel.connect(timeout: 1, read_timeout: 1, connect_timeout: 1, pool_timeout: 1, ...)

    Raises

    • Sequel::DatabaseConnectionError on connect and read timeouts
    • Sequel::PoolTimeout on checkout timeout

HTTP Clients

curb

curl = Curl::Easy.new(url)
curl.connect_timeout = 1
curl.timeout = 1
curl.perform

Raises Curl::Err::TimeoutError

em-http-client

EventMachine.run do
  http = EventMachine::HttpRequest.new(url, connect_timeout: 1, inactivity_timeout: 1).get
  http.errback  { http.error }
end

No exception is raised, but http.error is set to Errno::ETIMEDOUT in http.errback.

excon

Excon.get(url, connect_timeout: 1, read_timeout: 1, write_timeout: 1)

Raises Excon::Errors::Timeout

faraday

Faraday.get(url) do |req|
  req.options.open_timeout = 1
  req.options.timeout = 1
end

or

Faraday.new(url, request: {open_timeout: 1, timeout: 1}) do |faraday|
  # ...
end

Raises

  • Faraday::ConnectionFailed on connect timeout
  • Faraday::TimeoutError on read timeout

http

HTTP.timeout(connect: 1, read: 1, write: 1).get(url)

Raises HTTP::TimeoutError

httparty

HTTParty.get(url, timeout: 1)

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

httpclient

client = HTTPClient.new
client.connect_timeout = 1
client.receive_timeout = 1
client.send_timeout = 1
client.get(url)

Raises

  • HTTPClient::ConnectTimeoutError on connect timeout
  • HTTPClient::ReceiveTimeoutError on read timeout

httpi

HTTPI::Request.new(url: url, open_timeout: 1)

Raises same errors as underlying client

net/http

Net::HTTP.start(host, port, open_timeout: 1, read_timeout: 1) do
  # ...
end

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

open-uri

open(url, open_timeout: 1, read_timeout: 1)

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

patron

sess = Patron::Session.new
sess.connect_timeout = 1
sess.timeout = 1

Raises Patron::TimeoutError

rest-client

RestClient::Request.execute(method: :get, url: url, open_timeout: 1, timeout: 1)

Raises RestClient::RequestTimeout

typhoeus

response = Typhoeus.get(url, connecttimeout: 1, timeout: 1)

No exception is raised. Check for a timeout with

response.timed_out?

Web Servers

puma

# config/puma.rb
worker_timeout 15

Default: 30s

This kills and respawns the worker process. Note that this is for the worker and not threads. This isn’t a request timeout either. Use Rack middleware for request timeouts.

# config/puma.rb
worker_shutdown_timeout 8

Default: 60s

This causes Puma to send a SIGKILL signal to a worker if it hasn’t shutdown within the specified time period after having received a SIGTERM signal.

unicorn

# config/unicorn.rb
timeout 15

Default: 60s

This kills and respawns the worker process.

It’s recommended to use this in addition to Rack middleware.

Rack Middleware

rack-timeout

Rack::Timeout.timeout = 5
Rack::Timeout.wait_timeout = 5

Default: 15s service timeout, 30s wait timeout

Raises Rack::Timeout::RequestTimeoutError or Rack::Timeout::RequestExpiryError

Read more here

slowpoke

Slowpoke.timeout = 5

Default: 15s

Raises same exceptions as rack-timeout

External Services

actionmailer

Not configurable at the moment, and no timeout by default

bitly

Bitly.new(username, api_key, timeout)

Raises BitlyTimeout

firebase

firebase = Firebase::Client.new(url)
firebase.request.connect_timeout = 1
firebase.request.receive_timeout = 1
firebase.request.send_timeout = 1

Raises

  • HTTPClient::ConnectTimeoutError on connect timeout
  • HTTPClient::ReceiveTimeoutError on read timeout

gibbon

Gibbon::Request.new(timeout: 1, ...)

Raises Gibbon::MailChimpError

geocoder

Geocoder.configure(timeout: 1, ...)

No exception is raised by default. To raise exceptions, use

Geocoder.configure(timeout: 1, always_raise: :all, ...)

Raises Timeout::Error

hipchat

[HipChat::Client, HipChat::Room, HipChat::User].each { |c| c.default_timeout(1) }

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

koala

Koala.http_service.http_options = {request: {open_timeout: 1, timeout: 1}}

Raises

  • Faraday::ConnectionFailed on connect timeout
  • Faraday::TimeoutError on read timeout

mail

Not configurable at the moment, and no timeout by default, but there is a pull request

mechanize

agent = Mechanize.new
agent.open_timeout = 1
agent.read_timeout = 1

Raises

  • Net::OpenTimeout on connect timeout
  • Net::HTTP::Persistent::Error on read timeout

net/smtp

smtp = Net::SMTP.new(host, 25)
smtp.open_timeout = 1
smtp.read_timeout = 1

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

omniauth-oauth2

Not configurable at the moment, and no timeout by default

slack-notifier

Slack::Notifier.new(webhook_url, http_options: {open_timeout: 1, read_timeout: 1})

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

stripe

Stripe.open_timeout = 1
Stripe.read_timeout = 1

Default: 30s connect timeout, 80s read timeout

Raises Stripe::APIConnectionError

twilio-ruby

Twilio::REST::Client.new(account_sid, auth_token, timeout: 1)

Default: 30s

Raises

  • Net::OpenTimeout on connect timeout
  • Net::ReadTimeout on read timeout

twitter

Not configurable at the moment, and no timeout by default

zendesk_api

Not configurable at the moment

Default: 10s connect timeout, no read timeout

Don’t see a library you use?

Let us know. Even better, create a pull request for it.

Rescuing Exceptions

Take advantage of inheritance. Instead of

rescue Net::OpenTimeout, Net::ReadTimeout

you can do

rescue Timeout::Error

Use

  • Timeout::Error for both Net::OpenTimeout and Net::ReadTimeout
  • Faraday::ClientError for both Faraday::ConnectionFailed and Faraday::TimeoutError
  • HTTPClient::TimeoutError for both HTTPClient::ConnectTimeoutError and HTTPClient::ReceiveTimeoutError
  • Redis::BaseConnectionError for both Redis::CannotConnectError and Redis::TimeoutError
  • Rack::Timeout::Error for both Rack::Timeout::RequestTimeoutError and Rack::Timeout::RequestExpiryError

Existing Services

Adding timeouts to existing services can be a daunting task, but there’s a low risk way to do it.

  1. Select a timeout - say 5 seconds
  2. Log instances exceeding the proposed timeout
  3. Fix them
  4. Add the timeout
  5. Repeat this process with a lower timeout, until your target timeout is achieved

Running the Tests

git clone https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts.git
cd the-ultimate-guide-to-ruby-timeouts
bundle install
node test/server.js # in a separate window
rake

Bonus: PostgreSQL Statement Timeouts

Prevent single queries from taking up all of your database’s resources. Set a statement timeout in your config/database.yml

production:
  variables:
    statement_timeout: 250 # ms

or set it on your database role

ALTER ROLE myuser SET statement_timeout = 250;

Test statement timeouts with

SELECT pg_sleep(30);

And lastly...

Because time is not going to go backwards, I think I better stop now. - Stephen Hawking

🕓

Releases

No releases published

Packages

No packages published

Languages

  • Ruby 99.3%
  • JavaScript 0.7%