Net::ReadTimeout

Net::OpenTimeout's good-for-nothing brother.

Ruby's Net::ReadTimeout class

ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the response cannot be read within the read_timeout.

Superclass

Timeout::Error

Enemies

Honeybadger

Net::ReadTimeout is raised when a chunk of data cannot be read within a specified amount of time.

# read_timeout_1.rb
require 'net/http'

# create a new http connection object, the connection isn't made yet
c = Net::HTTP.new("www.example.com")

# set the read timeout to 1ms
# i.e. if we can't read the response or a chunk within 1ms this will cause a
# Net::ReadTimeout error when the request is made
c.read_timeout = 0.001

# make a GET request after opening a connection
response = c.request_get("/index.html")

# print the response status code
puts "RESPONSE STATUS CODE: #{response.code}"

$ ruby read_timeout_1.rb
# => ../net/protocol.rb:176:in `rbuf_fill': Net::ReadTimeout (Net::ReadTimeout)

When we execute this code, Net::ReadTimeout is raised because it takes more than 1 ms for the server to send a response after the connection is set up. It is important to set the read_timeout to a sensible number of seconds/milliseconds based on your requirements. The default value is 60 seconds.

» In the context of HTTP requests

A successful HTTP request has 3 main steps:

  1. Open a TCP connection to the endpoint.
  2. Send a request over the connection.
  3. Read the response written to the connection.

Net::ReadTimeout is raised when step 3 doesn't succeed within the given time.

This error is not the same as Net::OpenTimeout, which is raised when a connection cannot be setup within a given amount of time (i.e. when step 1 does not succeed within a given time).

If you (or the library that you use) don't define a read_timeout, your code will be stuck trying to read a response from a slow server for a long time when something is wrong with the network or the endpoint. So, setting these timeouts is very important to build predictable systems.

When you run into Net::ReadTimeout, you should handle it by retrying the request a few times, or giving up and showing a helpful error to the user:

# read_timeout_2.rb
require 'net/http'

def get(host, path, retries = 3)
  # create a new http connection object, the connection isn't made yet
  c = Net::HTTP.new(host)

  # set the read timeout to 1ms
  # i.e. if we can't read the response or a chunk within 1ms this will cause a
  # Net::ReadTimeout error when the request is made
  c.read_timeout = 0.001

  # make a GET request after opening a connection
  response = c.request_get("/index.html")

  # print the response status code
  puts "RESPONSE STATUS CODE: #{response.code}"

rescue Net::OpenTimeout => e
  puts "TRY #{retries}/n ERROR: timed out while trying to connect #{e}"
  if retries <= 1
    raise
  end
  get(host, path, retries - 1)
end

get("www.example.com", "/index.html")
# => ../net/protocol.rb:176:in `rbuf_fill': Net::ReadTimeout (Net::ReadTimeout)

» Too many Net::ReadTimeout errors

Ideally you shouldn't be getting too many read timeouts if read_timeout is set to a sensible value. If you get too many of these errors, it could indicate that:

  1. You have set a low read_timeout, which can be fixed by increasing the value.
  2. The target endpoint response times have a lot of deviation, in which case you must either send the traffic in a controlled way (by throttling it), or if the target endpoint is in your control, improve its response time.

» Usage in common gems

Most HTTP clients provide a way to configure read_timeout; here are a few of the common ones:

» 1. HTTParty

HTTParty allows you to set a read timeout using the code below:

# use a read_timeout of 100ms
HTTParty.get('http://www.example.com', { read_timeout: 0.1 })

# use it in a custom client
class SaneHTTPClient
  include HTTParty
  read_timeout 1
end

SaneHTTPClient.get("www.example.com")

» 2. Faraday

Faraday allows you to pass read timeout using the timeout option as part of the :request option hash, which throws a Faraday::TimeoutError error in case of a read timeout.

# set the read timeout to 1ms
conn = Faraday.new(url: "http://www.example.com", request: { timeout: 0.001 })
conn.get("/index.html")
# => Faraday::TimeoutError: Net::ReadTimeout

» 3. REST Client

REST Client offers read_timeout as an option to the RestClient::Request.execute function which throws a RestClient::Exceptions::ReadTimeout error in case of a read timeout.

# set the read timeout to 1ms
RestClient::Request.execute(method: :get, url: 'http://example.com/',
                            read_timeout: 0.001)
# => RestClient::Exceptions::ReadTimeout: Timed out reading data from server

» Pedantic Note

read_timeout usually specifies the time to read responses while making HTTP requests. However, in case of chunked responses, this timeout applies just for reading a single chunk of the response. So, if we have an HTTP server which streams data by sending a chunk every second and the whole response takes 10 minutes, setting a read_timeout to 2 seconds will not error out because we are receiving a chunk every second. So, don't be surprised if a read_timeout of 2 seconds doesn't error out even after 10 minutes while using chunked responses.

» More resources

The Ultimate Guide to Ruby Timeouts is a great article which documents how to add timeouts while using popular gems.