/ Tags: RAILS / Categories: RAILS

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_queue in an initializer and run bin/jobs start alongside 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.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More