Sidekiq uses middleware chains inspired by Rack to implement cross-cutting concerns like metrics, logging, and error tracking without modifying job code. Client middleware wraps job pushing; server middleware wraps job execution. This architectural pattern provides elegant extensibility while maintaining the single responsibility principle.

Architecture Pattern

Middleware follows the nested function pattern where each middleware wraps the next in the chain:

class TimingMiddleware
  def call(worker, job, queue)
    start = Time.now
    yield  # Continue to next middleware or job execution
    duration = Time.now - start
    logger.info "#{job['class']} took #{duration}s"
  end
end

The yield keyword creates the nesting—control flows down through the middleware stack to job execution, then back up as each middleware completes. This enables before/after behavior naturally:

Request → M1 before → M2 before → Job → M2 after → M1 after → Response

Client vs Server Middleware

Client middleware runs in the application process when enqueueing jobs. It can:

  • Validate or transform arguments
  • Add metadata to the job hash
  • Prevent job creation entirely by not yielding
  • Log job creation for audit trails
class ArgumentSanitizer
  def call(worker_class, job, queue, redis_pool)
    job['args'].map! { |arg| sanitize(arg) }
    yield
  end
end

Server middleware runs in the Sidekiq server process when executing jobs. It can:

  • Measure job duration
  • Handle exceptions and retry logic
  • Manage resources (database connections, locks)
  • Copy thread-local state
class TransactionMiddleware
  def call(worker, job, queue)
    ActiveRecord::Base.transaction do
      yield
    end
  end
end

Configuration

Middleware is configured globally and applied to all jobs:

Sidekiq.configure_client do |config|
  config.client_middleware do |chain|
    chain.add ArgumentSanitizer
  end
end
 
Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    chain.add TransactionMiddleware
    chain.add TimingMiddleware
  end
end

Order matters—middleware executes in the order added. Early middleware can prevent later middleware from running by not yielding.

Built-In Middleware

Sidekiq includes several critical middleware out of the box:

JobLogger: Logs job start and completion with timing information. Structured logging includes job class, JID, queue, and arguments.

RetryJobs: Implements exponential backoff for failed jobs. Catches exceptions, calculates next retry time, and updates the retry count in Redis.

CurrentAttributes: Copies Rails CurrentAttributes from enqueuing thread to executing thread. This preserves request context like current user or tenant ID across the async boundary.

# In controller
Current.user = @user
MyJob.perform_async  # Enqueues with Current.user preserved
 
# In job execution
class MyJob
  def perform
    Current.user  # Same user object available!
  end
end

Exception Handling

Middleware can customize exception handling without modifying the retry logic:

class SentryMiddleware
  def call(worker, job, queue)
    yield
  rescue => exception
    Sentry.capture_exception(exception, extra: job)
    raise  # Re-raise so retry logic still works
  end
end

By re-raising the exception, we send to Sentry while letting Sidekiq’s retry middleware handle scheduling. This separation of concerns keeps error reporting independent from retry logic.

Resource Management

Middleware excels at resource acquisition and cleanup patterns:

class ConnectionPoolMiddleware
  def call(worker, job, queue)
    external_api_pool.with do |client|
      Thread.current[:api_client] = client
      yield
    ensure
      Thread.current[:api_client] = nil
    end
  end
end

The middleware ensures the resource is available during job execution and cleaned up afterward, even if exceptions occur. This pattern eliminates boilerplate in every job class.

Performance Considerations

Each middleware adds overhead—a function call and potential memory allocations. For high-throughput systems, minimize middleware or optimize hot paths:

class ConditionalMiddleware
  def call(worker, job, queue)
    if job['expensive_feature']
      do_expensive_thing
    end
    yield
  end
end

Avoid expensive operations for every job. Conditionally execute based on job metadata or worker class.

Thread Safety

Middleware must be thread-safe since one instance is shared across all processor threads. Avoid instance variables for per-job state:

# BAD: instance variable shared across threads
class BrokenMiddleware
  def call(worker, job, queue)
    @job = job  # Thread-unsafe!
    yield
  end
end
 
# GOOD: local variable or thread-local storage
class SafeMiddleware
  def call(worker, job, queue)
    job_id = job['jid']  # Local variable, thread-safe
    yield
  end
end

For per-job state that must survive the yield, use thread-local storage: Thread.current[:state] = value.

See Sidekiq Architecture for how middleware fits into the overall job execution flow.