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:installThe 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
endConfigure 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
endCreating 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
endEnqueue 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
endQueue 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: 2Assign 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
endConcurrency 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
endThe 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_queueFor 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_KEYMonitoring 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
endClean 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
endCommon 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.