Asynchronous responses in Rack

Asynchronous responses in Rack

With the Rack synchronous interface protocol, the entire body must be prepared immediately, or be otherwise quickly available, for example by reading from file. A response that must be waited upon will tie up the process or thread executing the HTTP request. In a multi-process Rack server such as Unicorn, this will block the entire worker process, making in unavailable to server other requests.

Some Rack servers provide an alternate interface that allows the request to be suspended, unblocking the worker. At some later time, the request may be resumed, and the response sent to the client.

While there is not yet an async interface in the Rack specification, several Rack servers have implemented James Tucker's async scheme.

Rather than returning [status, headers, body], the app returns a status of -1, or throws the symbol :async.

The server provides env['async.callback'] which the app saves and later calls with the usual [status, headers, body] to send the response.

Note: returning a status of -1 is illegal as far as Rack::Lint is concerned. throw :async is not flagged as an error.

class AsyncApp
  def call(env) do
      sleep 5  # simulate waiting for some event
      response = [200, {'Content-Type' => 'text/plain'}, ['Hello, World!']]
      env['async.callback'].call response
    [-1, {}, []]  # or throw :async

In the example above, the request is suspended, nothing is sent back to the client, the connection remains open, and the client waits for a response.

The app returns the special status, and the worker process is able to handle more HTTP requests (i.e. it is not blocked). Later, inside the thread, the full response is prepared and sent to the client.

Casiano Rodriguez León 2015-01-07