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!
Data Stores
HTTP Clients
Web Servers
Rack Middleware
External Services
Bonus
ActiveRecord::Base.establish_connection(connect_timeout: 1, ...)or in config/database.yml
production:
connect_timeout: 1Raises PG::ConnectionBad
ActiveRecord::Base.establish_connection(connect_timeout: 1, read_timeout: 1, write_timeout: 1, ...)or in config/database.yml
production:
connect_timeout: 1
read_timeout: 1
write_timeout: 1Raises Mysql2::Error
PG.connect(connect_timeout: 1, ...)Raises PG::ConnectionBad
Mysql2::Client.new(connect_timeout: 1, read_timeout: 1, write_timeout: 1, ...)Raises Mysql2::Error
Dalli::Client.new(host, socket_timeout: 1, ...)Default: 0.5s
Raises Dalli::RingError
Redis.new(connect_timeout: 1, timeout: 1, ...)Raises
Redis::CannotConnectErroron connect timeoutRedis::TimeoutErroron read timeout
Mongo::Client.new([host], socket_timeout: 1, server_selection_timeout: 1, ...)Raises Mongo::Error::NoServerAvailable on connect timeout
TODO read timeout
Bunny.new(connection_timeout: 1, ...)Raises Bunny::TCPConnectionFailedForAllHosts on connect timeout
TODO read timeout
Elasticsearch::Client.new(transport_options: {request: {timeout: 1}}, ...)Raises
Faraday::ConnectionFailedon connect timeoutFaraday::TimeoutErroron read timeout
Searchkick.timeout = 1Default: 10s
Raises same exceptions as elasticsearch
Net::HTTP.start(host, port, open_timeout: 1, read_timeout: 1) do
# ...
endRaises
Net::OpenTimeouton connect timeoutNet::ReadTimeouton read timeout
open(url, open_timeout: 1, read_timeout: 1)Raises
Net::OpenTimeouton connect timeoutNet::ReadTimeouton read timeout
HTTP.timeout(connect: 1, read: 1, write: 1).get(url)Raises HTTP::TimeoutError
HTTParty.get(url, timeout: 1)Raises
Net::OpenTimeouton connect timeoutNet::ReadTimeouton read timeout
client = HTTPClient.new
client.connect_timeout = 1
client.receive_timeout = 1
client.send_timeout = 1
client.get(url)Raises
HTTPClient::ConnectTimeoutErroron connect timeoutHTTPClient::ReceiveTimeoutErroron read timeout
RestClient::Request.execute(method: :get, url: url, open_timeout: 1, timeout: 1)Raises RestClient::RequestTimeout
Faraday.get(url) do |req|
req.options.timeout = 1
req.options.open_timeout = 1
endRaises
Faraday::ConnectionFailedon connect timeoutFaraday::TimeoutErroron read timeout
curl = Curl::Easy.new(url)
curl.connect_timeout = 1
curl.timeout = 1
curl.performRaises Curl::Err::TimeoutError
response = Typhoeus.get(url, connecttimeout: 1, timeout: 1)No exception is raised. Check for a timeout with
response.timed_out?# config/unicorn.rb
timeout 15This kills and respawns the worker process.
It’s recommended to use this in addition to Rack middleware.
There’s no timeout option. Use Rack middleware instead.
Rack::Timeout.timeout = 5Default: 15s
Raises Rack::Timeout::RequestTimeoutError or Rack::Timeout::RequestExpiryError
Slowpoke.timeout = 5Default: 15s
Raises same exceptions as rack-timeout
Geocoder.configure(timeout: 1, ...)No exception is raised by default. To raise exceptions, use
Geocoder.configure(timeout: 1, always_raise: :all, ...)Raises Timeout::Error
Twilio::REST::Client.new(account_sid, auth_token, timeout: 1)Default: 30s
Raises
Net::OpenTimeouton connect timeoutNet::ReadTimeouton read timeout
Koala.http_service.http_options = {request: {open_timeout: 1, timeout: 1}}Raises
Faraday::ConnectionFailedon connect timeoutFaraday::TimeoutErroron read timeout
Not configurable at the moment, and no timeout by default
Stripe.open_timeout = 1
Stripe.read_timeout = 1Default: 30s connect timeout, 80s read timeout
Raises Stripe::APIConnectionError
Not configurable at the moment
Default: 10s connect timeout, no read timeout
[HipChat::Client, HipChat::Room, HipChat::User].each { |c| c.default_timeout(1) }Raises
Net::OpenTimeouton connect timeoutNet::ReadTimeouton read timeout
No official support yet, but this does the job
firebase = Firebase::Client.new(url)
firebase.request.instance_variable_get(:@client).connect_timeout = 1
firebase.request.instance_variable_get(:@client).receive_timeout = 1
firebase.request.instance_variable_get(:@client).send_timeout = 1Raises
HTTPClient::ConnectTimeoutErroron connect timeoutHTTPClient::ReceiveTimeoutErroron read timeout
Let us know. Even better, create a pull request for it.
Take advantage of inheritance. Instead of
rescue Net::OpenTimeout, Net::ReadTimeoutyou can do
rescue Timeout::ErrorUse
Timeout::Errorfor bothNet::OpenTimeoutandNet::ReadTimeoutFaraday::ClientErrorfor bothFaraday::ConnectionFailedandFaraday::TimeoutErrorHTTPClient::TimeoutErrorfor bothHTTPClient::ConnectTimeoutErrorandHTTPClient::ReceiveTimeoutErrorRedis::BaseConnectionErrorfor bothRedis::CannotConnectErrorandRedis::TimeoutErrorRack::Timeout::Errorfor bothRack::Timeout::RequestTimeoutErrorandRack::Timeout::RequestExpiryError
Adding timeouts to existing services can be a daunting task, but there’s a low risk way to do it.
- Select a timeout - say 5 seconds
- Log instances exceeding the proposed timeout
- Fix them
- Add the timeout
- Repeat this process with a lower timeout, until your target timeout is achieved
git clone https://github.com/ankane/ruby-timeouts.git
cd ruby-timeouts
bundle install
node test/server.js # in a separate window
rakePrevent 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 # msor set it on your database role
ALTER ROLE myuser SET statement_timeout = 250;Test statement timeouts with
SELECT pg_sleep(30);Because time is not going to go backwards, I think I better stop now. - Stephen Hawking
🕓