Middleware sits between the web server and Rails application, processing every request. While Rails 8 provides excellent built-in middleware, custom middleware solves problems that application-level code handles inefficiently—request timing, tenant detection, and request enrichment all benefit from middleware treatment.
Understanding the Middleware Stack
Rails middleware follows the Rack specification: each middleware receives a request, optionally modifies it, calls the next middleware, and optionally modifies the response. The stack executes top-to-bottom on requests and bottom-to-top on responses.
To inspect the current middleware stack:
# Terminal
bin/rails middleware
# Output shows order:
# use ActionDispatch::HostAuthorization
# use Rack::Sendfile
# use ActionDispatch::Executor
# ... (many more)
# run YourApp::Application.routesCustom middleware inserts into this stack at specific positions, giving control over when code executes relative to Rails internals.
Building Request Timing Middleware
Tracking request duration at the middleware level captures time spent across the entire stack, not just controller processing. This proves valuable for identifying slow middleware or establishing baselines.
# lib/middleware/request_timer.rb
module Middleware
class RequestTimer
def initialize(app, options = {})
@app = app
@header_name = options.fetch(:header_name, "X-Request-Time-Ms")
@threshold_ms = options.fetch(:threshold_ms, 1000)
end
def call(env)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, response = @app.call(env)
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
headers[@header_name] = duration_ms.to_s
if duration_ms > @threshold_ms
log_slow_request(env, duration_ms)
end
[status, headers, response]
end
private
def log_slow_request(env, duration_ms)
Rails.logger.warn(
"[SlowRequest] #{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
"took #{duration_ms}ms (threshold: #{@threshold_ms}ms)"
)
end
end
endRegister the middleware in the application configuration:
# config/application.rb
module YourApp
class Application < Rails::Application
config.load_defaults 8.0
# Insert early in stack to capture full request time
config.middleware.insert_before 0, Middleware::RequestTimer,
threshold_ms: 500,
header_name: "X-Response-Time"
end
endUsing Process.clock_gettime(Process::CLOCK_MONOTONIC) instead of Time.now prevents issues with system clock changes affecting measurements.
Tenant Detection Middleware for Multi-Tenant Apps
Multi-tenant applications need tenant identification early in the request cycle. Middleware handles this elegantly, setting up tenant context before any application code executes.
# lib/middleware/tenant_resolver.rb
module Middleware
class TenantResolver
TENANT_HEADER = "X-Tenant-ID".freeze
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
tenant_identifier = extract_tenant(request, env)
if tenant_identifier.nil?
return [400, { "Content-Type" => "application/json" },
[{ error: "Tenant identification required" }.to_json]]
end
tenant = find_tenant(tenant_identifier)
if tenant.nil?
return [404, { "Content-Type" => "application/json" },
[{ error: "Tenant not found" }.to_json]]
end
# Store tenant in request env for downstream access
env["app.current_tenant"] = tenant
env["app.tenant_id"] = tenant.id
@app.call(env)
end
private
def extract_tenant(request, env)
# Priority: header > subdomain > param
env["HTTP_#{TENANT_HEADER.upcase.tr('-', '_')}"] ||
extract_subdomain(request.host) ||
request.params["tenant_id"]
end
def extract_subdomain(host)
parts = host.split(".")
return nil if parts.length < 3
subdomain = parts.first
return nil if %w[www app api].include?(subdomain)
subdomain
end
def find_tenant(identifier)
# Cache tenant lookups to avoid database hits on every request
Rails.cache.fetch("tenant:#{identifier}", expires_in: 5.minutes) do
Tenant.find_by(slug: identifier) || Tenant.find_by(id: identifier)
end
end
end
endAccess the tenant from controllers or models through the request environment:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_tenant
private
def set_current_tenant
Current.tenant = request.env["app.current_tenant"]
end
end
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :tenant
endRequest Enrichment Middleware
Some applications need to attach computed data to requests—geographic information, feature flags, or A/B test assignments. Middleware centralizes this logic.
# lib/middleware/request_enricher.rb
module Middleware
class RequestEnricher
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# Attach request metadata
env["app.request_id"] = env["HTTP_X_REQUEST_ID"] || SecureRandom.uuid
env["app.client_ip"] = extract_client_ip(env)
env["app.user_agent_info"] = parse_user_agent(env["HTTP_USER_AGENT"])
env["app.request_started_at"] = Time.current
status, headers, response = @app.call(env)
# Add request ID to response for tracing
headers["X-Request-ID"] = env["app.request_id"]
[status, headers, response]
end
private
def extract_client_ip(env)
# Handle proxies and load balancers
forwarded = env["HTTP_X_FORWARDED_FOR"]
return forwarded.split(",").first.strip if forwarded.present?
env["HTTP_X_REAL_IP"] || env["REMOTE_ADDR"]
end
def parse_user_agent(user_agent)
return {} if user_agent.blank?
{
mobile: user_agent.match?(/Mobile|Android|iPhone/i),
bot: user_agent.match?(/bot|crawler|spider|crawling/i),
browser: extract_browser(user_agent)
}
end
def extract_browser(user_agent)
case user_agent
when /Chrome/i then "chrome"
when /Safari/i then "safari"
when /Firefox/i then "firefox"
when /Edge/i then "edge"
else "other"
end
end
end
endMiddleware Ordering and Conditional Execution
Middleware position matters. Insert timing middleware early to capture full duration. Insert authentication middleware after session restoration. Rails provides several insertion methods:
# config/application.rb
# Insert at specific position (0 = first)
config.middleware.insert_before 0, Middleware::RequestTimer
# Insert relative to existing middleware
config.middleware.insert_after ActionDispatch::Executor, Middleware::TenantResolver
# Insert at end (just before routes)
config.middleware.use Middleware::RequestEnricher
# Replace existing middleware
config.middleware.swap ActionDispatch::ShowExceptions, CustomExceptionHandler
# Remove middleware entirely
config.middleware.delete Rack::RuntimeFor environment-specific middleware, wrap registration in conditionals:
# config/application.rb
config.middleware.use Middleware::RequestTimer if Rails.env.production?
# Or use environment-specific files
# config/environments/production.rb
config.middleware.insert_before 0, Middleware::RequestTimer, threshold_ms: 200Testing Custom Middleware
Test middleware in isolation using Rack::MockRequest:
# spec/middleware/request_timer_spec.rb
require "rails_helper"
require "middleware/request_timer"
RSpec.describe Middleware::RequestTimer do
let(:app) { ->(env) { [200, {}, ["OK"]] } }
let(:middleware) { described_class.new(app, threshold_ms: 100) }
it "adds timing header to response" do
status, headers, body = middleware.call(Rack::MockRequest.env_for("/"))
expect(headers).to have_key("X-Request-Time-Ms")
expect(headers["X-Request-Time-Ms"].to_f).to be > 0
end
it "logs slow requests" do
slow_app = ->(env) { sleep(0.15); [200, {}, ["OK"]] }
slow_middleware = described_class.new(slow_app, threshold_ms: 100)
expect(Rails.logger).to receive(:warn).with(/SlowRequest/)
slow_middleware.call(Rack::MockRequest.env_for("/slow"))
end
endCommon Middleware Mistakes
Several patterns cause problems in production middleware:
- Blocking I/O in middleware—Database queries or HTTP calls slow every request. Cache aggressively or move to background processing.
- Modifying response body incorrectly—Response bodies are enumerable, not strings. Wrap modifications properly or use
Rack::Response. - Forgetting thread safety—Instance variables persist across requests in threaded servers. Store request-specific data in
env, not instance variables. - Swallowing exceptions—Let exceptions propagate to Rails exception handling unless intentionally catching specific errors.
Summary
Custom middleware handles cross-cutting concerns efficiently—timing, tenant resolution, and request enrichment execute once per request regardless of routing. The middleware stack provides precise control over execution order, and proper testing ensures reliability. For most applications, two to four custom middleware classes cover common needs without overcomplicating the request pipeline.