July 31, 2026
Middleware is one of the core building blocks of every Ruby on Rails application, yet many developers never interact with it directly. Every incoming HTTP request passes through a chain of middleware before it reaches your routes and controllers, and every response travels back through the same chain before being sent to the client.
Understanding how middleware works allows you to customize the request lifecycle, add cross-cutting concerns, and integrate application-wide behavior without modifying controllers or models.
This article explains what middleware is, how the Rails middleware stack works, how to configure it, and when to use the different middleware configuration methods.
What is Middleware?
A middleware is simply an object that sits between the web server and your Rails application.
Its job is to inspect, modify, or react to an HTTP request before it reaches your application, and optionally modify the response before it is returned to the client.
A simplified request flow looks like this:
Browser │ ▼Web Server (Puma) │ ▼Middleware A │Middleware B │Middleware C │Rails Routes │Controller │Model │Database │Response ▲Middleware C ▲Middleware B ▲Middleware A ▲Browser
Each middleware decides whether to:
- continue processing the request,
- modify the request,
- modify the response,
- or stop the request entirely.
Rack: The Foundation
Rails middleware is built on top of Rack.
A Rack middleware responds to a single method:
class MyMiddleware def initialize(app) @app = app end def call(env) @app.call(env) endend
The constructor receives the next application (or middleware) in the chain.
The call method receives the Rack environment (env) and returns a Rack response:
[ status, headers, body]
For example:
[ 200, { "Content-Type" => "text/plain" }, ["Hello World"]]
A Simple Middleware
Suppose we want to measure how long every request takes.
class RequestTimer def initialize(app) @app = app end def call(env) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) status, headers, body = @app.call(env) elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at Rails.logger.info "Request took #{elapsed.round(3)} seconds"
[status, headers, body]
end end
Notice that the middleware performs work before and after the application executes.
The Middleware Stack
Rails stores middleware in a stack.
You can inspect it with:
bin/rails middleware
Example output:
use ActionDispatch::HostAuthorizationuse Rack::Sendfileuse ActionDispatch::Staticuse Rack::Runtimeuse ActionDispatch::Executoruse ActionDispatch::RequestIduse ActionDispatch::RemoteIp...
Middleware are executed from top to bottom on the request, and bottom to top on the response.
Configuring Middleware
Rails exposes the middleware stack through:
Rails.application.config.middleware
This object provides several methods for manipulating the stack.
use
Adds middleware to the end of the stack.
config.middleware.use RequestTimer
If your middleware has constructor arguments:
config.middleware.use RequestTimer, :production
Rails internally does something equivalent to:
RequestTimer.new(next_app, :production)
insert_before
Inserts middleware before another middleware.
config.middleware.insert_before( Rack::Runtime, RequestTimer)
Before:
Rack::RuntimeRequestId
After:
RequestTimerRack::RuntimeRequestId
Use this when your middleware must execute earlier.
insert_after
Inserts middleware after another middleware.
config.middleware.insert_after( ActionDispatch::Executor, RequestTimer)
Before:
ExecutorRequestId
After:
ExecutorRequestTimerRequestId
This is commonly used when you want Rails to initialize its execution context first, but still execute your middleware very early.
delete
Removes middleware.
config.middleware.delete Rack::ETag
Useful when replacing default behavior.
swap
Replaces one middleware with another.
config.middleware.swap( Rack::Runtime, RequestTimer)
Result:
BeforeRack::RuntimeAfterRequestTimer
move_before
Moves an existing middleware.
config.middleware.move_before( Rack::Runtime, ActionDispatch::Executor)
No middleware is created.
Only its position changes.
move_after
Moves an existing middleware after another.
config.middleware.move_after( Rack::Runtime, ActionDispatch::Executor)
Again, only the order changes.
Constructor Arguments
All middleware configuration methods can receive constructor arguments.
Suppose your middleware is:
class RequestLogger def initialize(app, level, destination) @app = app @level = level @destination = destination end def call(env) @app.call(env) endend
You can configure it as:
config.middleware.use( RequestLogger, :debug, STDOUT)
Rails automatically forwards the arguments:
RequestLogger.new( next_app, :debug, STDOUT)
ActionDispatch::Executor
One middleware that appears in almost every Rails application is:
ActionDispatch::Executor
This middleware wraps every request inside the Rails Executor.
Conceptually, it behaves like:
Rails.application.executor.wrap do @app.call(env)end
Its responsibilities include:
- preparing the Rails execution context,
- managing thread-local state,
- integrating with the autoloader,
- cleaning request-specific state,
- ensuring each request starts from a clean environment.
You rarely interact with it directly, but many custom middleware are intentionally inserted immediately after it because it marks the beginning of Rails-managed execution.
How Middleware Executes
Imagine the stack contains:
ABCApplication
Execution order:
A before B before C before Controller C after B afterA after
Each middleware surrounds the next one.
This pattern is known as the Chain of Responsibility.
Returning Early
A middleware doesn’t have to continue processing.
For example:
class MaintenanceMode def initialize(app) @app = app end def call(env) if ENV["MAINTENANCE"] == "true" return [ 503, { "Content-Type" => "text/plain" }, ["Service Unavailable"] ] end @app.call(env) endend
If maintenance mode is enabled, Rails never reaches the controller.
Common Middleware Use Cases
Middleware is an excellent place for functionality that applies to every request.
Examples include:
- request logging,
- request timing,
- distributed tracing,
- rate limiting,
- custom authentication,
- feature flags,
- CORS handling,
- request ID generation,
- response header injection,
- maintenance mode,
- IP filtering,
- request auditing.
If your logic should execute for every request regardless of controller, middleware is often the right place.
Choosing the Right Configuration Method
MethodWhen to UseuseAdd new middleware to the end of the stack.insert_beforeExecute before a specific middleware.insert_afterExecute after a specific middleware.deleteRemove middleware from the stack.swapReplace an existing middleware.move_beforeReorder existing middleware.move_afterReorder existing middleware.
Final Thoughts
Middleware is one of Rails’ most powerful extension points. While controllers handle application-specific behavior, middleware is designed for request-wide concerns that should execute consistently across the entire application.
Understanding how middleware is ordered, how each configuration method affects the stack, and the role of components like ActionDispatch::Executor gives you greater control over the Rails request lifecycle. Once you become comfortable with the middleware stack, you’ll find that many problems can be solved more cleanly and efficiently at this layer than inside controllers or models.