Rack: what a Ruby web app is

call(env)

A Rack application is any object with one method: call(env) returns a status, a header hash and a body that responds to each. Sinatra, Rails and every Rack server speak this interface, which is why they fit together:

class Hello def call(env) [200, { "content-type" => "text/plain" }, ["hello #{env["PATH_INFO"]}"]] end end

env is a hash with the request: the method, the path, the query string, the headers and the input stream. The body is an array of strings, not one string, so a large response can be streamed in pieces.

config.ru

rackup reads config.ru, builds the application and starts a server. The file starts with the class from the section above and is Ruby; its two verbs are use for middleware and run for the app at the bottom of the stack:

class Timing def initialize(app) @app = app end def call(env) start = Process.clock_gettime(Process::CLOCK_MONOTONIC) status, headers, body = @app.call(env) us = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1_000_000).round(1) [status, headers.merge("x-runtime-us" => us.to_s), body] end end use Timing run Hello.new

use Timing wraps the app: a request enters the middleware, calls the app, and leaves with one extra header. Middleware can change the request, the response, or both, and the order of the use lines is the order of the layers.

Middleware

The interface for middleware is the same as for an app: an object with call(env) that holds the next one. That is the whole idea, and it is why a logging layer, a session layer and an auth layer can be composed without knowing about each other.

rackup and a request

gem install rack

1 gem installed

rackup -p 9294

[2026-11-05 00:28:49] INFO WEBrick 1.9.2 [2026-11-05 00:28:49] INFO ruby 3.4.12 (2026-10-23) [x86_64-linux-musl] [2026-11-05 00:28:49] INFO Rackup::Handler::WEBrick::Server#start: pid=624 port=9294

One request through the stack, read with the header the middleware added:

ruby -rnet/http -e 'r = Net::HTTP.get_response(URI("http://127.0.0.1:9294/hi")); puts r.code; puts r["x-runtime-us"]; puts r.body'

200 4.0 hello /hi

Four microseconds is the time inside the app, not the socket. A middleware that measured the round trip would sit above the server adapter instead; the position in the stack decides what is measured.

What to watch for

Rack 3 requires lower-case header names in the response hash. A mixed-case key is ignored by some servers and passed through by others, which is a bug that appears only in production.

The body must respond to each and is closed by the server. A body that holds a file handle needs a close method, or the descriptor leaks after the response; Rack calls it when it exists.

Next: A web app with Sinatra

Article author: Arthur Isaev