skip to content

In Ruby's Net::HTTP, what are the default open_timeout and read_timeout, how do you set them, and which exceptions signal them?

level: middleimportance: must knowfreq 52%

answer

  1. a full minute each
  2. open, read and write all 60
  3. one-shot helpers take no timeout
  4. options to Net::HTTP.start
  5. Net::OpenTimeout, Net::ReadTimeout < Timeout::Error

basics

~10 s

Both default to 60 seconds. Set them as options to Net::HTTP.start or through http.open_timeout= and read_timeout= before sending; expiry raises Net::OpenTimeout or Net::ReadTimeout, both subclasses of Timeout::Error.

solid answer

~30 s

A `Net::HTTP` object starts with `open_timeout`, `read_timeout` and `write_timeout` all at 60 seconds. `open_timeout` bounds opening the TCP connection and, for https, the TLS handshake; `read_timeout` bounds each wait for data from the socket; `write_timeout` bounds each write. The one-shot helpers `Net::HTTP.get`, `get_response` and `post` take no timeout arguments, so production code uses `Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 2, read_timeout: 5)`, which calls the matching setter for each option, or `Net::HTTP.new` plus `http.read_timeout = 5`. Expiry raises `Net::OpenTimeout`, `Net::ReadTimeout` or `Net::WriteTimeout`; all three inherit from `Timeout::Error`, a `RuntimeError`, so rescue them by name next to connection errors such as `Errno::ECONNREFUSED`.

code

ruby · 13 lines
ruby
require "net/http"

uri = URI("https://api.example.com/v1/forecast?city=Oslo")

begin
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
                        open_timeout: 2, read_timeout: 5) do |http|
    http.request(Net::HTTP::Get.new(uri))
  end
  puts res.body if res.is_a?(Net::HTTPSuccess)
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
  warn "weather service unavailable: #{e.class}"
end

go deeper

for a junior

Remember that Net::HTTP's open and read timeouts both default to 60 seconds and that the one-line helpers cannot change them.

for a middle

Explain what each timeout bounds, how start options map to setters, and the Net::OpenTimeout, Net::ReadTimeout and Timeout::Error hierarchy.

for a senior

Set short, configured timeouts on every dependency call, rescue the right classes, and reason about how a stalled dependency consumes web workers.

for a principal

Own a timeout budget across the call chain so each hop's limit fits inside its caller's, and make it reviewable configuration rather than per-call literals.

## Three timeouts, all generous A **timeout** is the longest a client waits for one step of a network call before giving up with an error. Every `Net::HTTP` object is created with these defaults, set in its initializer: | Setting | Default | What it bounds | Error on expiry | |---|---|---|---| | `open_timeout` | 60 s | opening the TCP connection, and the TLS handshake for https | `Net::OpenTimeout` | | `read_timeout` | 60 s | each wait for bytes from the server | `Net::ReadTimeout` | | `write_timeout` | 60 s | each wait to write request bytes | `Net::WriteTimeout` | | `keep_alive_timeout` | 2 s | how long an idle connection is trusted for reuse | none, it reconnects | A **minute** is fine for a script run by hand. It is far too long for a web request handler that calls a weather API: if the API stalls, the handler holds a worker thread or process for up to a minute per wait, and a burst of traffic can exhaust every worker while they all wait on the same slow dependency. ## Where you can set them The one-shot class methods `Net::HTTP.get`, `Net::HTTP.get_response`, `Net::HTTP.post` and `Net::HTTP.put` accept **no timeout arguments**. Their second argument is a path or a headers Hash, so `Net::HTTP.get(uri, read_timeout: 5)` does not set a timeout at all: the Hash is taken as request headers, and the Integer value even fails header setup with a `NoMethodError`. To set timeouts you control the object: 1. **Options to `Net::HTTP.start`.** `Net::HTTP.start(host, port, use_ssl: true, open_timeout: 2, read_timeout: 5)` creates the object and calls `open_timeout=`, `read_timeout=` and any other setter whose name matches an option key, before connecting. 2. **Setters on `Net::HTTP.new`.** Build with `http = Net::HTTP.new(host, port)`, assign `http.open_timeout = 2` and `http.read_timeout = 5`, then call `http.start` or `http.request`. 3. **During a session.** `read_timeout=` and `write_timeout=` also update the live socket, so you can tighten them between requests; `open_timeout` only matters before connecting. Values are seconds as Integer or Float (`0.5` works). `nil` removes the limit for that wait, which is rarely what you want. ## The exceptions - **`Net::OpenTimeout`**: the connection or TLS handshake did not complete in time. - **`Net::ReadTimeout`**: a single read waited longer than `read_timeout` for data. - **`Net::WriteTimeout`**: a single write could not proceed in time, typically while sending a large body. All three inherit from **`Timeout::Error`**, which is a `RuntimeError`, so a bare `rescue` catches them. Naming them is better: it documents intent and avoids swallowing bugs. Other transport failures are not timeouts: a port with nothing listening raises `Errno::ECONNREFUSED`, and a reset raises `Errno::ECONNRESET`. A resilient call usually rescues the timeout classes and those `Errno` classes together and maps them to one "dependency unavailable" outcome. ## write_timeout and uploads `write_timeout` matters when you send a large body, such as a batch of sensor readings to a weather service. Each write to the socket must make progress within the limit or `Net::WriteTimeout` is raised. For small JSON requests it almost never fires, which is why interviews focus on the open and read limits. open-uri passes its own `open_timeout:` and `read_timeout:` options straight through to the `Net::HTTP` object it builds, so the same classes are raised there; with no option given, the 60-second defaults apply to open-uri calls as well. ## Choosing values for a dependency call - Keep `open_timeout` short, often one or two seconds: a healthy server accepts connections fast, and a long wait here usually means it is down or unreachable. - Set `read_timeout` from the dependency's real latency, a little above its slow-but-healthy responses. - Remember that `read_timeout` limits **each read**, not the whole response, and that a timed-out GET may be retried once by default; both stretch the worst case beyond the number you set. - Keep the values in configuration, not scattered literals, so they can be tuned per environment. ## Checking what you actually have The values are plain readers, so the quickest proof is a console: `Net::HTTP.new("api.example.com").read_timeout` returns `60`. Ruby 4.0 ships Net::HTTP as the `net-http` 0.9.1 default gem, and its initializer is where these defaults live; reading them beats trusting a remembered number.

  • What does Net::HTTP.get(uri, read_timeout: 5) actually do in Ruby?
    It never sets a timeout. With a URI first, `get` treats a Hash second argument as request headers, so `read_timeout: 5` becomes a would-be header; building it calls `strip` on the Integer 5 and raises `NoMethodError`. A String value would simply be sent as a header while the read kept its 60-second default.
  • Does a bare rescue catch Net::ReadTimeout, and why name it anyway?
    Yes: `Net::ReadTimeout` inherits from `Timeout::Error`, a `RuntimeError`, which is under `StandardError`. Naming it, with `Net::OpenTimeout` and the connection `Errno` classes, keeps unrelated bugs such as a `NoMethodError` from being reported as a slow dependency.

saying these in an interview costs you the question

  • Net::HTTP waits forever by default unless you set a timeout
  • Net::HTTP.get(uri, read_timeout: 5) sets a five-second read timeout
  • keep_alive_timeout is how long Net::HTTP waits for the response
  • Net::ReadTimeout inherits from Exception, so rescue => e misses it
  • A refused connection raises Net::OpenTimeout after open_timeout seconds