rails background-jobs monitoring

Mission Control Jobs: Production Monitoring for Rails 8

- 18 min read

Set up Mission Control Jobs for production Rails 8 apps. Covers authentication, console API, bulk operations, alerting, and comparison with Sidekiq Web UI.

Mission Control Jobs dashboard for monitoring Solid Queue in Rails 8 production

Mission Control Jobs is the monitoring dashboard your Solid Queue setup is missing. Install it, mount it, and you get a production-ready web UI for inspecting queues, retrying failed jobs, and watching workers - all without adding another service to your stack.

If you’ve already set up Solid Queue or migrated from Sidekiq, Mission Control is the next step. Here’s everything you need to run it in production, including the console API that most guides skip entirely.

Mission Control vs Sidekiq Web UI vs GoodJob Dashboard

Mission Control provides free queue management with built-in multi-app support and a powerful console API. Sidekiq Pro offers superior real-time metrics and throughput graphs. GoodJob’s dashboard sits between them with solid charting at no cost. Here’s the full comparison:

Feature Mission Control Sidekiq Web UI GoodJob Dashboard
Price Free (MIT) Free (basic) / $99+ (Pro) Free (MIT)
Queue browsing Yes Yes Yes
Pause/unpause queues Yes Yes (Pro) No
Failed job retry Individual + bulk Individual + bulk Individual + bulk
Job argument inspection Yes (with filtering) Yes Yes
Worker monitoring Yes Yes Yes
Real-time metrics No Yes (Pro) Yes (charts)
Throughput graphs No Yes (Pro) Yes
Job search/filter By queue + class By queue + class + args By queue + class + args
Recurring job management View only Via sidekiq-cron Full CRUD
Console API Yes (powerful) Limited ActiveRecord queries
Multi-app support Yes (built-in) No No
Sensitive arg filtering Built-in config Manual Manual
Authentication HTTP Basic + custom Rack middleware Rack middleware

The dashboard choice usually follows the backend choice. If you’re still deciding between Solid Queue and Sidekiq, the Solid Queue setup guide covers the trade-offs in detail.

Installation

Add the gem and mount the engine:

# Gemfile
gem "mission_control-jobs"
# config/routes.rb
Rails.application.routes.draw do
  mount MissionControl::Jobs::Engine, at: "/jobs"

  # Your other routes...
end
bundle install

That’s it for development. Mission Control Jobs is maintained by 37signals (the team behind Basecamp and HEY) and reads directly from your Solid Queue database tables - no additional migrations, no separate database, no Redis. It requires Solid Queue 1.0.1 or higher.

Asset Pipeline Note

If you’re using Vite, jsbundling, or an API-only Rails app, you also need Propshaft for Mission Control’s assets:

# Gemfile - only needed if you don't already have an asset pipeline
gem "propshaft"

Then precompile in production:

RAILS_ENV=production rails assets:precompile

Most standard Rails 8 apps with Propshaft (the new default) won’t need this extra step.

Authentication in Production

Mission Control ships with HTTP Basic Auth enabled and locked down by default. No credentials configured means no access - it fails closed, which is the right default.

Option 1: HTTP Basic Auth (Simplest)

Generate credentials with the built-in task:

# Development
bin/rails mission_control:jobs:authentication:configure

# Production
RAILS_ENV=production bin/rails mission_control:jobs:authentication:configure

This stores credentials in Rails encrypted credentials:

# config/credentials.yml.enc (after decryption)
mission_control:
  http_basic_auth_user: admin
  http_basic_auth_password: your-secure-password

Or set them manually in an initializer:

# config/initializers/mission_control.rb
Rails.application.configure do
  config.mission_control.jobs.http_basic_auth_user = Rails.application.credentials.dig(:mission_control, :http_basic_auth_user)
  config.mission_control.jobs.http_basic_auth_password = Rails.application.credentials.dig(:mission_control, :http_basic_auth_password)
end

HTTP Basic Auth works fine for small teams and solo developers. It’s what I use on most projects where I’m the only one checking the dashboard.

Rails 8 ships with a built-in authentication generator. If you’re already using it, plug Mission Control into the same auth flow:

# app/controllers/admin_controller.rb
class AdminController < ApplicationController
  before_action :require_admin

  private

  def require_admin
    # Use Rails 8 authentication
    redirect_to root_path unless authenticated? && Current.user.admin?
  end
end
# config/environments/production.rb
config.mission_control.jobs.base_controller_class = "AdminController"
config.mission_control.jobs.http_basic_auth_enabled = false

This gives you proper session-based authentication with your existing user model. Users log in through your normal login flow and get access to Mission Control if they’re admins.

Option 3: Devise

Same pattern, different auth library:

# app/controllers/admin_controller.rb
class AdminController < ApplicationController
  before_action :authenticate_user!
  before_action :require_admin_role

  private

  def require_admin_role
    redirect_to root_path, alert: "Not authorized" unless current_user.admin?
  end
end
# config/environments/production.rb
config.mission_control.jobs.base_controller_class = "AdminController"
config.mission_control.jobs.http_basic_auth_enabled = false

IP Restriction (Extra Layer)

For production, consider adding IP restrictions on top of authentication:

# app/controllers/admin_controller.rb
class AdminController < ApplicationController
  before_action :restrict_ip
  before_action :authenticate_user!

  private

  def restrict_ip
    allowed_ips = ENV.fetch("ADMIN_ALLOWED_IPS", "").split(",")
    unless allowed_ips.empty? || allowed_ips.include?(request.remote_ip)
      head :forbidden
    end
  end
end

What the Dashboard Shows You

Once mounted, you get four main views at /jobs:

Queues Tab

Lists all your Solid Queue queues with pending job counts. You can:

  • See how many jobs are waiting in each queue
  • Pause a queue (stops workers from picking up new jobs)
  • Unpause a queue (resumes processing)
  • Click into a queue to browse individual pending jobs

Queue pausing is the feature I use most during deployments. Pause the queue, deploy, verify the new code works, then unpause. No jobs lost, no race conditions.

Failed Jobs Tab

Shows every job that raised an unhandled exception. For each failed job you see:

  • Job class name
  • Queue it was running on
  • Error class and message
  • Full backtrace (with Rails backtrace cleaning)
  • Job arguments (with optional filtering for sensitive data)
  • When it failed

You can retry individual jobs or select multiple jobs for bulk retry/discard.

In-Progress Jobs

Shows jobs currently being executed by workers. Useful for identifying:

  • Long-running jobs that might be stuck
  • Which workers are busy vs idle
  • Whether a specific job class is monopolizing your workers

Workers Tab

Lists all active Solid Queue worker processes with their:

  • Process ID and hostname
  • Queue assignments
  • Currently executing job (if any)

The Console API

Mission Control extends ActiveJob with a query interface you can use in the Rails console to filter, retry, and discard jobs in bulk. Run ActiveJob.jobs.failed.where(job_class_name: "SomeJob").retry_all to retry thousands of failed jobs in one command. The web dashboard covers 80% of what you need day-to-day - the console API covers the other 20%, the incident response and bulk operations that matter most when things go wrong.

Start a Rails console and you get immediate access:

bin/rails console
# => Type 'jobs_help' to see available servers

Querying Jobs

# All failed jobs
ActiveJob.jobs.failed
# => Returns a relation-like object you can chain

# Failed jobs for a specific class
ActiveJob.jobs.failed.where(job_class_name: "PaymentProcessorJob")

# Pending jobs in a specific queue
ActiveJob.jobs.pending.where(queue_name: "critical")

# Scheduled jobs (waiting for their run time)
ActiveJob.jobs.scheduled

# Currently executing jobs
ActiveJob.jobs.in_progress

# Finished jobs (if you have Solid Queue's finished job retention enabled)
ActiveJob.jobs.finished

# Pagination
ActiveJob.jobs.failed.limit(10).offset(0)

Bulk Operations

This is where the console API saves you during incidents:

# Retry ALL failed jobs
ActiveJob.jobs.failed.retry_all

# Retry only failed jobs of a specific class
ActiveJob.jobs.failed.where(job_class_name: "EmailDeliveryJob").retry_all

# Discard all failed jobs in a queue (they're not coming back)
ActiveJob.jobs.failed.where(queue_name: "low_priority").discard_all

# Discard pending jobs of a specific class
# Useful when you deployed a broken job and need to clear the queue
ActiveJob.jobs.pending.where(job_class_name: "BrokenJob").discard_all

For large bulk operations, add a delay between batches to avoid hammering your database:

# Process in batches with a 2-second pause between each
MissionControl::Jobs.delay_between_bulk_operation_batches = 2.seconds
ActiveJob.jobs.failed.retry_all

Production Incident Example

Here’s a real scenario: you deployed a code change that broke OrderSyncJob. Thousands of jobs failed before you noticed. Here’s the recovery:

# 1. See the damage
ActiveJob.jobs.failed.where(job_class_name: "OrderSyncJob").count
# => 3,847

# 2. Check a sample to confirm it's the same error
ActiveJob.jobs.failed.where(job_class_name: "OrderSyncJob").limit(5).each do |job|
  puts "#{job.job_id}: #{job.error.message}"
end

# 3. Deploy the fix first, then retry in batches
MissionControl::Jobs.delay_between_bulk_operation_batches = 3.seconds
ActiveJob.jobs.failed.where(job_class_name: "OrderSyncJob").retry_all
# => Jobs retry in batches of 1000 with 3-second pauses

Filtering Sensitive Arguments

Job arguments often contain data you don’t want visible in a dashboard - API keys, tokens, personal information. Mission Control has built-in argument filtering:

# config/initializers/mission_control.rb
Rails.application.configure do
  config.mission_control.jobs.filter_arguments = [
    :password,
    :token,
    :api_key,
    :secret,
    :ssn,
    :credit_card
  ]
end

Filtered arguments show as [FILTERED] in both the web UI and console output. This works the same way as Rails parameter filtering - any argument key matching the list gets masked.

Building Alerting Around Mission Control

Here’s the gap nobody talks about: Mission Control is a dashboard, not a monitoring system. It shows you job failures when you look at it. It does not page you at 3am when your payment processing queue backs up.

You need to build that layer yourself. Here are three approaches:

Approach 1: ActiveSupport Notifications + Error Tracker

The simplest approach - let your error tracker (Sentry, Honeybadger, Bugsnag) handle job failure alerting:

# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
  # Solid Queue doesn't auto-retry, so this is your retry policy
  retry_on StandardError, wait: :polynomially_longer, attempts: 3

  # After all retries exhausted, report to error tracker
  discard_on StandardError do |job, error|
    Rails.error.report(error, context: {
      job_class: job.class.name,
      job_id: job.job_id,
      queue: job.queue_name,
      arguments: job.arguments
    }, severity: :error)
  end
end

Your error tracker already has alerting, PagerDuty integration, and deduplication. Use what you have.

Approach 2: Health Check Endpoint

Add a health check that monitoring tools can poll:

# app/controllers/health_controller.rb
class HealthController < ApplicationController
  # GET /health/jobs
  def jobs
    checks = {
      failed_jobs: SolidQueue::FailedExecution.count,
      blocked_jobs: SolidQueue::BlockedExecution.count,
      oldest_pending: SolidQueue::ReadyExecution.minimum(:created_at),
      workers_active: SolidQueue::Process.where(kind: "Worker").count
    }

    # Alert if too many failures or queue is backing up
    healthy = checks[:failed_jobs] < 100 &&
              checks[:workers_active] > 0 &&
              (checks[:oldest_pending].nil? || checks[:oldest_pending] > 10.minutes.ago)

    render json: checks.merge(healthy: healthy),
           status: healthy ? :ok : :service_unavailable
  end
end

Point your uptime monitor (Pingdom, UptimeRobot, or even a simple cron curl) at this endpoint. A 503 response triggers your alert.

Approach 3: Recurring Monitoring Job

Use Solid Queue’s own recurring jobs to monitor itself:

# app/jobs/queue_health_check_job.rb
class QueueHealthCheckJob < ApplicationJob
  queue_as :monitoring

  def perform
    failed_count = SolidQueue::FailedExecution.count
    oldest_pending = SolidQueue::ReadyExecution.minimum(:created_at)

    if failed_count > 50
      AdminMailer.job_alert(
        subject: "#{failed_count} failed jobs in queue",
        details: failed_job_summary
      ).deliver_now  # deliver_now, not deliver_later!
    end

    if oldest_pending && oldest_pending < 15.minutes.ago
      AdminMailer.job_alert(
        subject: "Job queue backing up - oldest job #{time_ago_in_words(oldest_pending)} old",
        details: queue_depth_summary
      ).deliver_now
    end
  end

  private

  def failed_job_summary
    SolidQueue::FailedExecution
      .joins(:job)
      .group("solid_queue_jobs.class_name")
      .count
      .sort_by { |_, count| -count }
      .first(10)
      .map { |klass, count| "#{klass}: #{count}" }
      .join("\n")
  end

  def queue_depth_summary
    SolidQueue::ReadyExecution
      .joins(:job)
      .group("solid_queue_jobs.queue_name")
      .count
      .map { |queue, count| "#{queue}: #{count} pending" }
      .join("\n")
  end
end
# config/recurring.yml
queue_health_check:
  class: QueueHealthCheckJob
  schedule: every 5 minutes

Notice deliver_now instead of deliver_later - if your job queue is the thing that’s broken, you don’t want to enqueue another job to send the alert.

Configuration Reference

Here are all the Mission Control settings worth tuning for production:

# config/initializers/mission_control.rb
Rails.application.configure do
  # Authentication
  config.mission_control.jobs.http_basic_auth_enabled = true
  config.mission_control.jobs.base_controller_class = "AdminController"

  # Filter sensitive job arguments from the UI
  config.mission_control.jobs.filter_arguments = [:password, :token, :api_key]

  # Limit count queries to prevent slow page loads on large tables
  # Default: 500,000 - lower this if your dashboard is slow
  config.mission_control.jobs.internal_query_count_limit = 100_000

  # Mark scheduled jobs as "delayed" after this threshold
  # Default: 1 minute
  config.mission_control.jobs.scheduled_job_delay_threshold = 5.minutes

  # Batch size for queries and bulk operations
  # Default: 1000
  config.active_job.default_page_size = 1000

  # Delay between bulk operation batches (retry_all, discard_all)
  # Default: 0 (no delay) - increase for large bulk ops
  config.mission_control.jobs.delay_between_bulk_operation_batches = 0
end

Performance Tuning

The internal_query_count_limit setting matters most for production. Mission Control runs count queries on your job tables to show queue depths. With millions of rows, these queries get slow. The default cap of 500,000 means Mission Control shows “500,000+” instead of running a full table scan.

If your dashboard loads slowly, lower this:

config.mission_control.jobs.internal_query_count_limit = 50_000

Multi-App Monitoring

One feature that sets Mission Control apart: you can monitor multiple applications from a single dashboard. If you run several Rails apps with Solid Queue, configure them all in one central monitoring app:

# config/initializers/mission_control.rb

# Register multiple apps
MissionControl::Jobs.applications.add(
  "main_app",
  { primary: ActiveJob::QueueAdapters.lookup(:solid_queue).new }
)

MissionControl::Jobs.applications.add(
  "billing_service",
  { primary: ActiveJob::QueueAdapters.lookup(:solid_queue).new }
)

This is useful for teams running a modular monolith or multiple services that all use Solid Queue. One dashboard, multiple queue backends.

Production Checklist

Before deploying Mission Control to production:

  • Authentication configured (not using default empty credentials)
  • filter_arguments set for any sensitive job data (tokens, PII, API keys)
  • internal_query_count_limit tuned if you have large job tables
  • Alerting configured separately (error tracker, health check, or monitoring job)
  • IP restrictions considered for the /jobs route
  • scheduled_job_delay_threshold set to match your SLA expectations
  • Tested bulk retry/discard in staging before production incident

Trade-offs and Limitations

What Mission Control Does Well

  • Zero-configuration monitoring for Solid Queue
  • Clean, focused UI that doesn’t overwhelm
  • Console API for incident response is genuinely powerful
  • Multi-app support out of the box
  • Argument filtering for compliance

What It Lacks

  • No real-time metrics - you can’t see throughput trends, processing times, or queue depth over time. Sidekiq Pro’s real-time dashboard is significantly better for performance tuning.
  • No alerting - it’s purely reactive. You need to build alerting separately.
  • No job search by arguments - you can filter by queue and class, but not by specific argument values. Investigating “what happened to user 12345’s job” requires the console.
  • No historical data - once a job is processed and cleaned up, it’s gone from Mission Control. There’s no retention or historical view unless you configure Solid Queue to keep finished jobs.
  • Limited recurring job management - you can view recurring jobs but can’t create, edit, or toggle them from the UI. Changes require editing recurring.yml and redeploying.

When Mission Control Is Not Enough

If you need real-time performance dashboards, consider pairing Mission Control with:

  • Application Performance Monitoring (Datadog, New Relic, Scout) for throughput metrics and latency tracking
  • Error tracking (Sentry, Honeybadger) for job failure alerting and investigation
  • Custom dashboards (Grafana + PostgreSQL queries) for historical job metrics

Mission Control handles the “what’s happening right now” and “fix this broken job” workflows. APM handles “how is our job system performing over time.”

The Bottom Line

Mission Control Jobs fills the operational gap between “I set up Solid Queue” and “I can manage it in production.” The web dashboard handles daily monitoring, the console API handles incidents, and the whole thing runs on your existing database with zero additional infrastructure.

It’s not Sidekiq Pro’s dashboard - it’s simpler, less real-time, and you need to build alerting yourself. But for the price (free) and the setup effort (one gem, two lines of config), it’s the obvious choice for any Solid Queue deployment.

What’s Next?

If you haven’t set up Solid Queue yet, start with the setup guide. If you’re migrating from Sidekiq, the migration guide covers the full cutover process including how to handle the dashboard transition.


Need help with production monitoring for Rails background jobs? I help teams with job infrastructure, operational resilience, and Solid Queue adoption. If you’re setting up monitoring or scaling your job system, reach out at nikita.sinenko@gmail.com.

Further Reading