In a Rack app, how would you write a middleware that measures each request's duration and adds it as a response header?
answer
- initialize receives the inner app
- call(env) delegates to @app.call(env)
- monotonic clock, local variables only
- lower-case header name in Rack 3
- use it in config.ru; Rack::Runtime exists
basics
~20 sA Rack middleware is a class whose initialize takes the inner app and whose call(env) reads a monotonic clock, calls @app.call(env), writes the elapsed time into the returned headers under a lower-case name, and returns the triple; config.ru adds it with use.
solid answer
~40 sA middleware is itself a Rack app that wraps another one. `Rack::Builder` instantiates it once as `Timing.new(app, *args)`, so `initialize` stores `@app`. In `call(env)` I take `Process.clock_gettime(Process::CLOCK_MONOTONIC)`, call `@app.call(env)`, compute the difference and set a header such as `headers["server-timing"]` - lower-case, because Rack 3 forbids uppercase header names - then return `[status, headers, body]`. Per-request values stay in local variables, since one instance serves every request and thread. I register it with `use Timing` before `run` in `config.ru`. Rack already ships `Rack::Runtime`, which writes `x-runtime` the same way. Note the limit: this times the stack until the triple comes back, not the time spent streaming the body; for that, hook `close` with `Rack::BodyProxy` or push a callback onto `env["rack.response_finished"]`.
code
ruby · 14 linesclass RequestTiming
def initialize(app, label: "app")
@app = app
@label = label
end
def call(env)
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, body = @app.call(env)
ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000
headers["server-timing"] = format("%s;dur=%.1f", @label, ms)
[status, headers, body]
end
endgo deeper
Recall the two methods of a middleware, initialize(app) and call(env), and that config.ru adds it with use before run.
Write the middleware from memory: monotonic clock, call inward, lower-case header, return the triple. Explain that one instance serves all requests, so state stays in locals.
Explain what the number measures depending on stack position and why streaming bodies escape it; use Rack::BodyProxy or rack.response_finished for end-to-end timing, and reach for Rack::Runtime before writing your own.
Decide where timing belongs: a header for quick debugging, logs or metrics for real latency data, and which layers must be inside the measurement for the number to answer the question the team is asking.
## What a middleware is A **Rack middleware** is a Rack application that holds a reference to another Rack application. The server only ever calls the outermost object; each middleware decides what to do before and after it passes the request inward. Structurally it needs two methods: - `initialize(app, *args)` - `Rack::Builder` calls this **once**, when it builds the stack, passing the next app inward as the first argument and any extra arguments from the `use` line after it. - `call(env)` - called on **every request**. It usually calls `@app.call(env)` and returns the triple it gets back, possibly edited. ## The timing middleware, step by step ```ruby class RequestTiming def initialize(app, label: "app") @app = app @label = label end def call(env) started = Process.clock_gettime(Process::CLOCK_MONOTONIC) status, headers, body = @app.call(env) ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 headers["server-timing"] = format("%s;dur=%.1f", @label, ms) [status, headers, body] end end ``` 1. **Read a monotonic clock** before calling inward. `Time.now` is wall-clock time and can jump when the system clock is adjusted; `Process::CLOCK_MONOTONIC` only moves forward. `Rack::Utils.clock_time`, which `Rack::Runtime` uses, makes this same call wherever the platform supports it. 2. **Call the inner app** and destructure the triple. 3. **Write the header in lower case.** Rack 3's SPEC forbids uppercase characters in response header names, and `Rack::Lint` raises on `Server-Timing`. 4. **Return a triple.** Returning the same body object is correct: this middleware never iterates it. In `config.ru`: ```ruby require_relative "request_timing" require_relative "app" use RequestTiming, label: "app" run App.new ``` `Rack::Builder#use` forwards positional arguments, keyword arguments and a block to `new`. ## State: locals, not instance variables The Builder creates **one instance per `use` line** and reuses it for the life of the process. A multi-threaded server such as Puma calls that same instance from several threads at once. Storing `@started = ...` inside `call` lets one request overwrite another's start time. Per-request data belongs in **local variables** or in the `env` Hash (under a key with a dot and your own prefix); instance variables are for configuration set in `initialize`. ## What the number actually measures Where you put the `use` line decides what gets timed: | Position in config.ru | Includes | |---|---| | First `use` (outermost) | every other middleware plus the app | | Last `use`, just before `run` | only the endpoint app | Even outermost, the timer stops when `@app.call(env)` **returns the triple**. For an Array body that is nearly the whole job; for a streaming or lazily-enumerated body the server has not yet sent a byte. Two ways to time the full response: - Wrap the body in **`Rack::BodyProxy.new(body) { ... }`**. Its block runs once, when the server calls `close` on the body after sending it. You cannot put the result in a header then, because headers are already sent, so log it instead. - Push a callable onto **`env["rack.response_finished"]`** when the server provides that Array. The server calls each entry with `env, status, headers, error` after the response is finished, in reverse order, and they must not raise. ## Variations interviewers probe - **Exceptions.** If the inner app raises, `@app.call(env)` never returns and no header is set. To log the duration of failed requests too, measure in a `begin`/`ensure` block and write to `env["rack.errors"]` or a logger, then let the exception propagate. - **Skipping paths.** A health-check endpoint polled every second can drown the data; check `env["PATH_INFO"]` and call straight through for it. - **Sharing the start time.** Inner layers can read a value you store in `env` under your own dotted key, such as `"timing.started"`; the SPEC reserves undotted keys for CGI variables and the `rack.` prefix for Rack itself. - **Configuration.** Anything passed on the `use` line arrives in `initialize` once, which is where thresholds or labels belong. ## Rack already has one: Rack::Runtime `Rack::Runtime` is the shipped version of this middleware. It measures with `Utils.clock_time`, formats seconds with `"%0.6f"` and writes them to `x-runtime`; `Rack::Runtime.new(app, "db")` writes `x-runtime-db` instead, so several can be stacked. It **does not overwrite** a header that an inner layer already set. Its own source comment gives the placement rule from the table above: right before the app to time the app, or before all other middleware to include them. ## Testing it ```ruby require "rack" app = RequestTiming.new(->(env) { [200, {}, ["ok"]] }) res = Rack::MockRequest.new(app).get("/", lint: true) res.get_header("server-timing") # e.g. "app;dur=0.0" ``` `Rack::MockRequest#get` builds an env, calls the app (through `Rack::Lint` when `lint: true`), and closes the body for you.
- Your timing middleware sets @started in call and the numbers look random under load. Why?`Rack::Builder` creates one middleware instance per `use` line and a threaded server calls it concurrently, so every request writes the same `@started`. One request reads another's start time. Keep per-request values in local variables inside `call`, or in `env` under your own dotted key, and reserve instance variables for configuration.
- Why can't you put the full streaming time into a response header?Headers are sent before the body. When the timer is stopped in `Rack::BodyProxy`'s close block or in a `rack.response_finished` callback, the status line and headers have already gone out, so the value can only be logged or sent to metrics.
- What happens if an inner layer already set x-runtime when Rack::Runtime runs?`Rack::Runtime` checks `headers.key?` first and leaves an existing value alone. To stack several timers, give each a name: `use Rack::Runtime, "db"` writes `x-runtime-db`, so the outer and inner measurements do not collide.
saying these in an interview costs you the question
- Storing the request start time in an instance variable of the middleware
- Measuring elapsed time with Time.now in a request timer
- Believing the timer includes the time spent streaming the body
- Calling body.each inside the middleware to measure the full response
- Writing the header as X-Response-Time under Rack 3