Solid Queue — Background Jobs in Rails 8 Without Redis
For years, adding background jobs to a Rails application meant adding Redis. Sidekiq is excellent, but it pulls in a dependency that needs its own infrastructure, its own monitoring, its own operational overhead. Rails 8 changes the calculus with Solid Queue — a database-backed job queue that ships as the default Active Job backend. For a huge range of applications, the database you already have is all the infrastructure you need.
What Solid Queue Is
Solid Queue stores jobs in your database rather than in Redis. It’s designed by the Rails team to handle high-throughput workloads, uses SELECT ... FOR UPDATE SKIP LOCKED (or equivalent) for concurrent job claiming without race conditions, and supports the full Active Job interface so your job classes stay unchanged.
It ships with Rails 8 and can be added to Rails 7.1+ as a gem. The key tradeoff is simple: you trade Redis infrastructure for database rows. For most applications, that’s a good trade.
Setup:
# Gemfile (Rails 7.1)
gem "solid_queue"
# Rails 8 — already included, just configure
# Install migrations
bin/rails solid_queue:install:migrations
bin/rails db:migrate
# Start the job processor
bin/jobs start
Setup:
# config/application.rb or config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
That’s the full setup for basic usage. The queue processor runs as a separate process, just like Sidekiq’s workers.
Defining and Enqueuing Jobs
Solid Queue is an Active Job adapter, so job definitions look exactly like any other Rails job.
Example:
class WelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end
# Enqueue immediately
WelcomeEmailJob.perform_later(user.id)
# Enqueue with delay
WelcomeEmailJob.set(wait: 10.minutes).perform_later(user.id)
# Enqueue at a specific time
WelcomeEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(user.id)
The perform_later interface is unchanged. Any existing Active Job code works without modification when switching to Solid Queue.
Queues and Priorities
Solid Queue supports multiple named queues and numeric priorities within those queues.
Example:
# config/queue.yml
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "default"
threads: 3
processes: 1
polling_interval: 0.1
- queues: "critical"
threads: 5
processes: 1
polling_interval: 0.1
- queues: "bulk_email,reporting"
threads: 2
processes: 1
polling_interval: 2
Example:
class CriticalAlertJob < ApplicationJob
queue_as :critical
def perform(alert_id)
Alert.find(alert_id).notify_oncall!
end
end
class BulkReportJob < ApplicationJob
queue_as :bulk_email
def perform(report_id)
Report.find(report_id).generate_and_send!
end
end
Workers pick up jobs only from their assigned queues. Critical jobs get dedicated worker threads; bulk work runs on a slower polling interval so it doesn’t starve higher-priority queues.
Recurring Jobs
Solid Queue includes built-in support for recurring (cron-style) jobs — no separate cron gem required.
Example:
# config/queue.yml
default: &default
dispatchers:
- recurring_tasks:
cleanup_expired_sessions:
class: CleanupExpiredSessionsJob
args: []
schedule: "0 3 * * *" # daily at 3am
sync_external_data:
class: ExternalDataSyncJob
args: [full]
schedule: "*/15 * * * *" # every 15 minutes
weekly_digest:
class: WeeklyDigestJob
args: []
schedule: "0 9 * * 1" # every Monday at 9am
Example:
class CleanupExpiredSessionsJob < ApplicationJob
queue_as :default
def perform
Session.where("expires_at < ?", Time.current).delete_all
end
end
The dispatcher process picks up the cron schedule and enqueues jobs at the right times. No external scheduler needed.
Concurrency Controls
Solid Queue has built-in concurrency controls that prevent multiple instances of the same job from running simultaneously. This is one of the features that sets it apart from basic queue implementations.
Example:
class AccountSyncJob < ApplicationJob
queue_as :default
# Only one sync per account at a time
limits_concurrency to: 1, key: ->(account_id) { "account-sync-#{account_id}" }
def perform(account_id)
Account.find(account_id).sync_from_external_api!
end
end
Example:
# Duration controls how long the concurrency lock is held
# Use when jobs can take a variable amount of time
class DataImportJob < ApplicationJob
queue_as :default
limits_concurrency to: 3,
key: :data_imports,
duration: 30.minutes
def perform(import_id)
DataImport.find(import_id).process!
end
end
Without this, queueing 50 sync jobs for the same account means 50 jobs hammering the external API simultaneously. limits_concurrency turns that into sequential or throttled execution.
Visibility and Monitoring
Solid Queue stores jobs in database tables, so you have full SQL access to job state. The Mission Control gem (also from the Rails team) provides a web UI.
Example:
# Gemfile
gem "mission_control-jobs"
# config/routes.rb
authenticate :user, ->(user) { user.admin? } do
mount MissionControl::Jobs::Engine, at: "/jobs"
end
Example:
# Inspect job state directly via SQL / ActiveRecord
SolidQueue::Job.failed.count
SolidQueue::Job.pending.where(queue_name: "critical").count
SolidQueue::Job.where("scheduled_at > ?", Time.current).order(:scheduled_at).first(10)
# Failed jobs include error details
SolidQueue::Job.failed.each do |job|
puts "#{job.class_name}: #{job.last_error}"
end
The database-backed approach means your existing database monitoring, backups, and tooling cover your job queue automatically.
Pro-Tip: When switching a production Rails app to Solid Queue, set
config.active_job.queue_adapter = :solid_queuein an initializer and runbin/jobs startalongside your existing setup first. Watch the queue depth and job timing for a week before decommissioning Redis. The database can handle more throughput than most teams expect — Basecamp runs Solid Queue at significant scale — but confirming your specific workload behaves well is worth the overlap period.
Conclusion
Solid Queue removes Redis from the list of required infrastructure for background job processing in Rails 8. The Active Job interface means zero changes to job code, database-backed storage means no extra infrastructure, and built-in recurring jobs and concurrency controls cover common patterns that used to require additional gems. For greenfield apps and for existing apps that want to reduce operational complexity, Solid Queue is worth evaluating seriously before assuming Sidekiq is required.
FAQs
Q1: Should I replace Sidekiq with Solid Queue in production?
It depends on your job volume and existing infrastructure. Sidekiq handles extremely high throughput and has a mature ecosystem. If you’re already running Redis and Sidekiq works well, there’s no urgent reason to switch. If you’re greenfield or want to reduce infrastructure complexity, Solid Queue is a strong choice.
Q2: Does Solid Queue work with PostgreSQL and MySQL?
Yes. Solid Queue works with PostgreSQL, MySQL, and SQLite. It uses SELECT ... FOR UPDATE SKIP LOCKED on databases that support it (PostgreSQL and MySQL 8+) for efficient job claiming.
Q3: How does Solid Queue handle failed jobs?
Failed jobs are moved to a failed jobs table with the error message and backtrace. You can inspect, retry, or discard them via Mission Control or directly via ActiveRecord. You can also configure retry_on in your job class just like any other Active Job adapter.
Q4: Can I use Solid Queue with multiple databases?
Yes. Rails 8’s Solid Queue supports database configuration via config/queue.yml. You can point it at a dedicated database separate from your main application database if you want to isolate job load.
Q5: What’s the difference between Solid Queue and Good Job?
Both are database-backed Active Job adapters. Good Job is a mature, well-tested gem with a long track record. Solid Queue is the Rails 8 default, backed by the Rails team, and designed with the same database-as-infrastructure philosophy. Either is a good choice; Solid Queue has the advantage of being the official Rails direction.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀