Timeouts for popular Ruby gems
Timeouts for popular Ruby gems
An unresponsive service can be 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!
Also available for Python, Node, Go, PHP, and Rust
For many apps, the single most important thing to do (if you use a relational database)
Standard Library
Data Stores
HTTP Clients
Commands
Web Servers
Rack Middleware
Solvers
Distributed Locks
3rd Party Services
Other
Prevent single queries from taking up all of your database’s resources.
If you use Rails, add to your config/database.yml
production:
variables:
statement_timeout: 5s # or ms, min, etc
or set it on your database role
ALTER ROLE myuser SET statement_timeout = '5s';
Test with
SELECT pg_sleep(6);
To set for a single transaction, use
BEGIN;
SET LOCAL statement_timeout = '5s';
...
COMMIT;
For migrations, you likely want to set a longer statement timeout. You can do this with
production:
variables:
statement_timeout: <%= ENV["STATEMENT_TIMEOUT"] || "5s" %>
And use
STATEMENT_TIMEOUT=90s rails db:migrate
Note: Only applies to read-only SELECT statements (more info)
If you use Rails, add to your config/database.yml
production:
variables:
max_execution_time: 5000 # ms
or set it directly on each connection
SET SESSION max_execution_time = 5000;
Test with
SELECT 1 FROM information_schema.tables WHERE sleep(6);
To set for a single statement, use an optimizer hint
SELECT /*+ MAX_EXECUTION_TIME(5000) */ ...
If you use Rails, add to your config/database.yml
production:
variables:
max_statement_time: 5 # sec
or set it directly on each connection
SET SESSION max_statement_time = 5;
Test with
SELECT 1 FROM information_schema.tables WHERE sleep(6);
To set for a single statement, use
SET STATEMENT max_statement_time=5 FOR
SELECT ...
For migrations, you likely want to set a longer statement timeout. You can do this with
production:
variables:
max_statement_time: <%= ENV['MAX_STATEMENT_TIME'] || 5 %>
And use
MAX_STATEMENT_TIME=90 rails db:migrate
Note: Requires Ruby 3.2+
STDIN.timeout = 1
Raises IO::TimeoutError
Net::FTP.new(host, open_timeout: 1, read_timeout: 1)
Raises
Net::OpenTimeout on connect timeoutNet::ReadTimeout on read timeoutNet::HTTP.start(host, port, open_timeout: 1, read_timeout: 1, write_timeout: 1) do
# ...
end
or
http = Net::HTTP.new(host, port)
http.open_timeout = 1
http.read_timeout = 1
http.write_timeout = 1
Raises
Net::OpenTimeout on connect timeoutNet::ReadTimeout on read timeoutNet::WriteTimeout on write timeoutDefault: 60s connect timeout, 60s read timeout, 60s write timeout
Read timeouts are retried once automatically for idempotent methods like GET. You can set the max number of retries with http.max_retries = 1.
Net::IMAP.new(host, open_timeout: 1)
Read timeout is not configurable at the moment
Raises Net::OpenTimeout on connect timeout
pop = Net::POP.new(host)
pop.open_timeout = 1
pop.read_timeout = 1
Raises
Net::OpenTimeout on connect timeoutNet::ReadTimeout on read timeoutsmtp = Net::SMTP.new(host, 25)
smtp.open_timeout = 1
smtp.read_timeout = 1
Raises
Net::OpenTimeout on connect timeoutNet::ReadTimeout on read timeoutURI.parse(url).open(open_timeout: 1, read_timeout: 1)
Raises
Net::OpenTimeout on connect timeoutNet::ReadTimeout on read timeoutNote: Requires Ruby 3.2+
Regexp.timeout = 1
# or
Regexp.new(regexp, timeout: 1)
Raises Regexp::TimeoutError
Socket.tcp(host, 80, connect_timeout: 1) do |sock|
# ...
end
Raises Errno::ETIMEDOUT
ActiveRecord::Base.establish_connection(connect_timeout: 1, checkout_timeout: 1, ...)
or in config/database.yml
production:
connect_timeout: 1
checkout_timeout: 1
Raises
ActiveRecord::ConnectionNotEstablished on connect and read timeoutsActiveRecord::ConnectionTimeoutError on checkout timeoutSee also PostgreSQL statement timeouts
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
ActiveRecord::ConnectionNotEstablished on connect and read timeoutsActiveRecord::ConnectionTimeoutError on checkout timeoutSee also MySQL statement timeouts
Bunny.new(connection_timeout: 1, read_timeout: 1, ...)
Raises
Bunny::TCPConnectionFailedForAllHosts on connect timeoutBunny::NetworkFailure on read timeoutCassandra.cluster(connect_timeout: 1, timeout: 1)
Default: 10s connect timeout, 12s read timeout
Raises
Cassandra::Errors::NoHostsAvailable on connect timeoutCassandra::Errors::TimeoutError on read timeoutConnectionPool.new(timeout: 1) { ... }
Raises ConnectionPool::TimeoutError
CouchRest.new(url, open_timeout: 1, read_timeout: 1, timeout: 1)
Raises
HTTPClient::ConnectTimeoutError on connect timeoutHTTPClient::ReceiveTimeoutError on read timeoutDalli::Client.new(host, socket_timeout: 1, ...)
Default: 1s
Raises Dalli::RingError
Drill.new(url: url, open_timeout: 1, read_timeout: 1)
Default: 3s connect timeout, no read timeout
Raises
Net::OpenTimeout on connect timeoutNet::ReadTimeout on read timeoutElasticsearch::Client.new(transport_options: {request: {timeout: 1}}, ...)
Raises Elastic::Transport::Transport::Error
conn = Hiredis::Connection.new
conn.timeout = 1_000_000 # microseconds
Raises
Errno::ETIMEDOUT on connect timeoutErrno::EAGAIN on read timeoutImmudb::Client.new(host, timeout: 1)
Raises GRPC::DeadlineExceeded
InfluxDB::Client.new(open_timeout: 1, read_timeout: 1)
Raises InfluxDB::ConnectionError
InfluxDB2::Client.new(url, token, open_timeout: 1, read
No open issues yet, or sync has not completed.