rubyguides

Rails Middleware: Request and Response Stack

Before you start

Middleware is the bridge between your application and the HTTP requests it receives. Every request that hits your Rails application passes through a series of Rails middleware layers before reaching your controller, and every response passes through those same layers on its way back out. Understanding how to use and create middleware gives you control over request processing without pushing every concern into controllers.

The useful mental model is that middleware sits in front of the app like a checkpoint. Some middleware inspects the request and passes it along. Some middleware changes headers or cookies. Some middleware stops the request early and returns a response without touching the controller stack at all. Once you see that flow, the stack becomes easier to place in the right part of the app.

You will learn what middleware is in the Rails context, how the middleware stack works, how to build custom middleware, and when to use it versus other approaches like controllers or concerns.

For a broader Rails overview, see the Rails routing tutorial and the Rails MVC pattern tutorial.

Middleware often feels abstract until you place it on the request path. A request arrives, middleware can inspect or adjust it, the controller runs, and the response travels back through the same stack on the way out. Once you picture that flow, middleware becomes less mysterious and easier to place in the right part of the app.

It also helps to think of middleware as infrastructure code. It is good at handling concerns that apply to many requests, such as logging, cookies, sessions, timing, and headers. It is not the best place for business rules that only matter to one controller action.

tl;dr

Use middleware for cross-cutting concerns that should run before or after most requests. Keep it small, keep it focused, and avoid putting business logic there unless every request truly needs it. If a concern only affects one controller or one model, that usually belongs somewhere else.

What Is Middleware?

Rails inherits its middleware architecture from Rack, a specification that standardizes how Ruby web frameworks interact with web servers. A middleware is simply a component that sits between the server and your application, able to inspect and modify both incoming requests and outgoing responses.

When a request arrives, it enters the middleware stack at the top and flows downward through each middleware layer. Each middleware can choose to pass the request to the next layer, return a response immediately without forwarding, or modify the request before passing it along. The same happens in reverse for the response.

Rails ships with numerous built-in middleware components. ActionDispatch::Static serves static files. ActionDispatch::Cookies handles cookie serialization. ActionDispatch::Session manages session data. ActionDispatch::ShowExceptions catches errors and renders friendly error pages. Rack::Runtime adds an X-Runtime header showing request duration. These work together to handle cross-cutting concerns that would be messy to implement in controllers.

The Rails middleware stack

You can inspect your application’s middleware stack using the Rails rake task:

rails middleware

You will see output similar to the listing below. The stack order is significant because each middleware wraps everything below it. Middleware higher in the list runs first on the way in and last on the way out. This ordering determines which components can inspect or modify the request before others get a chance.

You’ll see something like this:

use ActionDispatch::Static
use ActionDispatch::Executor
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use ActionDispatch::Static
use ActionDispatch::Files
use ActionDispatch::Executor
use ActionDispatch::Static
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run MyApp::Application.routes

The order matters significantly. ShowExceptions wraps everything below it, so any exceptions from later middleware get caught and displayed. Cookies must come before Session because sessions rely on cookies. Understanding this order helps you debug issues and place your own middleware correctly.

That order also tells you where to place your own code. If your middleware needs to wrap the whole request, insert it high in the stack. If it depends on cookies or session data, place it lower. The stack is less about memorizing every built-in component and more about knowing where your behavior belongs in the path.

Creating custom middleware

Creating your own middleware is straightforward. A middleware is simply a class that responds to call(env), where env is a hash containing request information. The method returns an array of three elements: status code, headers hash, and response body.

Here’s a simple middleware that adds a custom header to every response:

class AddTimestampHeader
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    headers['X-Generated-At'] = Time.now.iso8601
    [status, headers, body]
  end
end

To add this middleware to your application, create a file in app/middleware/ and then configure it in config/application.rb. The use method places your middleware at the bottom of the stack, just before the routes. That position is usually correct for response-modifying middleware like header injectors or timers. If you need your middleware to run earlier, use insert_before to place it above a specific built-in component.

Configure it in config/application.rb:

module MyApp
  class Application < Rails::Application
    config.middleware.use AddTimestampHeader
  end
end

The use method adds middleware to the stack. By default, it places the middleware at the bottom of the stack, just before your routes. You can insert middleware at specific positions using insert_before, insert_after, or prepend.

When you write custom middleware, keep the responsibility narrow. Good examples include adding a response header, timing a request, or short-circuiting a known special case. If the class starts to look like a controller, a service object, or a whole workflow, the logic probably belongs somewhere else.

Middleware for request logging

A practical use case is logging all requests with timing information. This helps you identify slow requests and unusual traffic patterns:

class RequestLogger
  def initialize(app, logger = Rails.logger)
    @app = app
    @logger = logger
  end

  def call(env)
    request = Rack::Request.new(env)
    start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)

    @logger.info "Started #{request.request_method} #{request.path} " \
                 "from #{request.remote_ip}"

    status, headers, body = @app.call(env)

    duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
    @logger.info "Completed #{status} in #{'%.2f' % duration}ms"

    [status, headers, body]
  end
end

This middleware measures actual processing time using monotonic clock time, which is more reliable than wall clock time for performance measurement. It logs the HTTP method, path, client IP, response status, and duration. That pattern is useful because it stays out of controller code and applies uniformly across the app. You can add similar middleware for request IDs, feature-flag headers, or lightweight debugging without changing each controller separately. The monotonic clock is important here because wall clock time can jump forward or backward when the system adjusts for NTP, which would produce meaningless duration values. The structured log format shown above makes it easy to pipe the output into a log aggregator for later analysis.

Conditional middleware

Sometimes you only want middleware to run for certain requests. You can wrap your middleware logic with conditionals:

class AdminOnlyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)

    if request.path.start_with?('/admin')
      # Admin-specific logic here
      # This runs before the rest of the stack
    end

    @app.call(env)
  end
end

Alternatively, use Rails’ built-in middleware configuration to apply conditions:

config.middleware.use AdminOnlyMiddleware, only: ['/admin', '/admin/*']
config.middleware.use AdminOnlyMiddleware, except: ['/api/*']

This approach keeps your middleware focused while allowing flexible application.

Summary

Rails middleware sits between the web server and your application, which makes it a good place for request-wide concerns like logging, sessions, cookies, CORS, and error handling. If the code should affect every request or most requests, middleware is usually the right fit. If it belongs to one action or one model, keep it elsewhere.

When to use middleware

Middleware excels at handling cross-cutting concerns that affect many or all requests: authentication, logging, request timing, header manipulation, cookies, sessions, and CORS. If something needs to happen for nearly every request, middleware is usually the right place.

However, middleware isn’t always the best solution. For controller-specific concerns, use filters or concerns. For model-specific logic, keep it in the model. For view-related processing, consider helpers or view decorators. Middleware should handle infrastructure-level concerns, not business logic.

One common mistake is putting too much logic in middleware, which makes debugging difficult since the stack order determines execution. If your middleware is doing complex work, consider whether that logic belongs elsewhere.

Debugging middleware issues

When things go wrong with middleware, the stack trace alone won’t tell you much because middleware executes before your controllers are even invoked. Here are some debugging strategies:

First, use rails middleware to verify your middleware is actually loaded and in the right position. Second, temporarily disable other middleware to isolate issues:

# Disable all middleware except yours
config.middleware = ActionDispatch::MiddlewareStack.new do |m|
  m.use YourMiddleware
end

Third, add logging directly in your middleware to trace request flow:

def call(env)
  Rails.logger.debug "Middleware entering: #{env['PATH_INFO']}"
  result = @app.call(env)
  Rails.logger.debug "Middleware exiting: #{result[0]}"
  result
end

Those three lines of logging are often enough to find a middleware issue without reaching for a debugger. The pattern works because middleware is just Ruby, and you can inspect env at any point in the stack. Once you see which middleware is setting or modifying a particular header, the fix is usually quick. This approach scales well to production because you can toggle the logging with an environment check instead of leaving debug output in every request.

Next steps

Once middleware feels natural, the next step is to compare it with the controller, view, and routing layers it surrounds. The Rails routing tutorial shows how a request first enters the app, and the Rails views and partials tutorial shows how the response is rendered after the controller finishes. That connection makes the middleware stack easier to remember because it has a clear place in the full request path. The Rails MVC pattern tutorial shows how the router and controllers fit into the same request cycle.

If you ever catch yourself adding business rules to middleware, step back and check whether a controller, concern, or service object would be clearer. Middleware is best at infrastructure concerns like headers, cookies, timing, or request shaping, not application logic that only belongs in one feature.

See Also