Solid Queue in Rails 8: Background Jobs

Rails 8 introduced Solid Queue as the default background job backend, replacing the need for Redis-based solutions like Sidekiq or Resque. This database-backed queue system simplifies deployment and reduces infrastructure complexity while handling most workloads efficiently.

Why Solid Queue Matters

Background jobs are essential for any production Rails application. Sending emails, processing uploads, generating reports—these tasks shouldn't block web requests. Previously, this meant setting up Redis and choosing between Sidekiq, Resque, or Delayed Job. Solid Queue eliminates that external dependency by using your existing MySQL database as the queue backend.

The benefits are significant: one less service to monitor, no Redis memory management, and transactional integrity with your application data. Jobs can be enqueued within the same database transaction as the data they operate on, preventing orphaned jobs when transactions roll back.

Setting Up Solid Queue

New Rails 8 applications come with Solid Queue pre-configured. For existing applications, add the gem and run the installer:

# Gemfile
gem "solid_queue"

# Terminal
bundle install
bin/rails solid_queue:install

The installer creates the necessary migration and configuration files. Run the migration to create the queue tables:

# db/migrate/20251231000000_create_solid_queue_tables.rb
class CreateSolidQueueTables < ActiveRecord::Migration[8.0]
  def change
    create_table :solid_queue_jobs do |t|
      t.string :queue_name, null: false
      t.string :class_name, null: false
      t.text :arguments
      t.integer :priority, default: 0, null: false
      t.string :active_job_id
      t.datetime :scheduled_at
      t.datetime :finished_at
      t.string :concurrency_key
      t.timestamps

      t.index [:queue_name, :finished_at]
      t.index [:active_job_id]
      t.index [:scheduled_at, :finished_at]
    end

    create_table :solid_queue_claimed_executions do |t|
      t.references :job, null: false
      t.bigint :process_id
      t.datetime :created_at, null: false

      t.index [:process_id, :job_id]
    end

    create_table :solid_queue_processes do |t|
      t.string :kind, null: false
      t.datetime :last_heartbeat_at, null: false
      t.bigint :supervisor_id
      t.integer :pid, null: false
      t.string :hostname
      t.text :metadata
      t.timestamps
    end
  end
end

Configure Active Job to use Solid Queue as its adapter:

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.active_job.queue_adapter = :solid_queue
  end
end

Creating and Enqueueing Jobs

Jobs in Solid Queue work exactly like standard Active Job classes. The framework handles serialization, execution, and retry logic automatically:

# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
  queue_as :default
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  discard_on ActiveJob::DeserializationError

  def perform(order_id)
    order = Order.find(order_id)
    
    OrderProcessor.new(order).tap do |processor|
      processor.validate_inventory
      processor.charge_payment
      processor.send_confirmation
      processor.update_analytics
    end
    
    order.update!(processed_at: Time.current)
  end
end

Enqueue jobs from controllers, models, or anywhere in the application:

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    @order = current_user.orders.build(order_params)
    
    if @order.save
      # Enqueue immediately
      ProcessOrderJob.perform_later(@order.id)
      
      # Or schedule for later
      ReminderJob.set(wait: 24.hours).perform_later(@order.id)
      
      redirect_to @order, notice: "Order placed successfully"
    else
      render :new, status: :unprocessable_entity
    end
  end
end

Queue Configuration and Priorities

Solid Queue supports multiple queues with different priorities and concurrency settings. Configure these in the queue configuration file:

# config/solid_queue.yml
default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "critical"
      threads: 5
      polling_interval: 0.1
    - queues: "default"
      threads: 3
      polling_interval: 1
    - queues: "low"
      threads: 1
      polling_interval: 5

development:
  <<: *default

production:
  <<: *default
  workers:
    - queues: "critical"
      threads: 10
      polling_interval: 0.1
    - queues: "default"
      threads: 5
      polling_interval: 0.5
    - queues: "low"
      threads: 2
      polling_interval: 2

Assign jobs to specific queues based on urgency:

# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
  queue_as :critical
  
  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

# app/jobs/generate_report_job.rb
class GenerateReportJob < ApplicationJob
  queue_as :low
  
  def perform(report_id)
    report = Report.find(report_id)
    ReportGenerator.new(report).generate_pdf
  end
end

Concurrency Control

Solid Queue provides built-in concurrency control to prevent duplicate job execution. This proves essential for operations that shouldn't run simultaneously for the same resource:

# app/jobs/sync_inventory_job.rb
class SyncInventoryJob < ApplicationJob
  queue_as :default
  limits_concurrency to: 1, key: ->(product_id) { "inventory_sync_#{product_id}" }
  
  def perform(product_id)
    product = Product.find(product_id)
    InventorySyncService.new(product).sync_with_warehouse
  end
end

The concurrency key ensures only one inventory sync runs per product at any time. Additional enqueued jobs for the same product wait until the current execution completes.

Running Workers in Production

Start Solid Queue workers using the provided binary or through the Puma plugin for single-server deployments:

# Option 1: Standalone worker process
# bin/jobs
#!/usr/bin/env ruby
require_relative "../config/environment"
SolidQueue::Supervisor.start

# Option 2: Puma plugin (for single-server setups)
# config/puma.rb
plugin :solid_queue

For Kamal deployments, add a separate worker service:

# config/deploy.yml
service: myapp
image: myapp

servers:
  web:
    hosts:
      - 192.168.1.1
  worker:
    hosts:
      - 192.168.1.1
    cmd: bin/jobs

env:
  clear:
    RAILS_ENV: production
  secret:
    - RAILS_MASTER_KEY

Monitoring and Maintenance

Solid Queue stores job data in the database, making monitoring straightforward with standard Active Record queries:

# lib/tasks/queue_stats.rake
namespace :queue do
  desc "Display queue statistics"
  task stats: :environment do
    pending = SolidQueue::Job.where(finished_at: nil).count
    failed = SolidQueue::FailedExecution.count
    processed_today = SolidQueue::Job
      .where("finished_at > ?", Time.current.beginning_of_day)
      .count
    
    puts "Pending jobs: #{pending}"
    puts "Failed jobs: #{failed}"
    puts "Processed today: #{processed_today}"
  end
end

Clean up old completed jobs periodically to prevent table bloat:

# app/jobs/cleanup_old_jobs_job.rb
class CleanupOldJobsJob < ApplicationJob
  queue_as :low
  
  def perform(days_to_keep = 7)
    cutoff = days_to_keep.days.ago
    
    SolidQueue::Job
      .where("finished_at < ?", cutoff)
      .in_batches(of: 1000)
      .delete_all
  end
end

Common Pitfalls

Several mistakes frequently appear when implementing background jobs. Passing entire Active Record objects instead of IDs causes serialization issues and stale data problems—always pass primitive values and reload objects within the job. Long-running jobs should be broken into smaller chunks or use batch processing to avoid timeout issues. Jobs should also be idempotent when possible, since retries may execute the same job multiple times.

Summary

Solid Queue brings production-ready background job processing to Rails 8 without external dependencies. The database-backed approach simplifies deployment while providing essential features: multiple queues, priority handling, concurrency control, and scheduled jobs. For applications already using MySQL, this means one less service to manage and better transactional guarantees between application data and job enqueuing.

Applications with extremely high job throughput or specialized requirements may still benefit from Redis-based solutions, but Solid Queue handles the majority of use cases efficiently. Start with Solid Queue and migrate only if specific performance requirements demand it.

14 claps
← Back to Blog