← All posts

May 20, 2026

Add Solid Queue to a Rails app in 10 minutes

A short, copy-pasteable walkthrough for swapping in Solid Queue as your Active Job backend, with the dev-mode Puma plugin and a real Job class at the end.

rails solid-queue background-jobs how-to

Solid Queue is the database-backed Active Job adapter that ships as a default in Rails 8 and works fine on Rails 7. If you’ve been running Sidekiq or Resque and the Redis dependency is more cost than benefit, it’s the simplest swap I’ve made in a Rails app in years.

This is the ten-minute version. Production tuning (concurrency limits, recurring schedules, separate worker fleets) is a separate post; this one gets you to a working background queue you can ship.

1. Add the gem

bundle add solid_queue

That’s the dependency.

2. Run the installer

bin/rails generate solid_queue:install
bin/rails db:migrate

The installer creates a config/queue.yml, sets config.active_job.queue_adapter = :solid_queue in config/application.rb (or in the appropriate environment file — verify after generation), and adds a migration with the tables Solid Queue needs: solid_queue_jobs, solid_queue_ready_executions, solid_queue_claimed_executions, solid_queue_failed_executions, and a handful of supporting tables for scheduled jobs, recurring tasks, and pauses.

In production, you’ll want these on a separate database from your application data — the queue tables get hot, and you don’t want lock contention on solid_queue_claimed_executions slowing down your users table. Rails 7+ multi-database support handles this; the docs in rails/solid_queue walk through it. For a small app, the same database is fine.

3. Run the worker

bin/jobs

That’s the worker process. By default it reads config/queue.yml to figure out which queues to work and how many threads to use, polls the database every 0.1 seconds, and processes jobs as they arrive. In production this is a long-running process you supervise like any other (Render’s worker service, Kamal’s accessory, systemd, foreman, whatever).

The default config the installer drops looks roughly like this:

# rails/solid_queue · config/queue.yml (default) · @176721e
default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 3
      processes: 1
      polling_interval: 0.1

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

One dispatcher (which moves jobs from “scheduled in the future” to “ready to run”), one worker process with three threads, working all queues. For a small app, that’s enough.

4. The Puma plugin (development only)

In development, running bin/jobs in a second terminal is a hassle. Solid Queue ships a Puma plugin that runs the worker in-process alongside the web server, so a single bin/dev boots both.

In config/puma.rb:

# illustrative
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] == "true"

Then in your Procfile.dev:

web: SOLID_QUEUE_IN_PUMA=true bin/rails server

Now bin/dev boots Puma, and Puma boots Solid Queue inside its own process. One terminal, two roles. I keep this off in production — workers should be their own process, separately scaled, separately monitored.

5. A real Job class

Active Job is unchanged; Solid Queue is the adapter behind it. If you’ve written ActiveJob::Base subclasses before, nothing about how you write jobs changes.

A small example I’d actually run:

# illustrative
class WelcomeEmailJob < ApplicationJob
  queue_as :default

  retry_on Net::ReadTimeout, wait: :polynomially_longer, attempts: 5
  discard_on ActiveJob::DeserializationError

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

A few things worth calling out.

queue_as :default. Solid Queue can route jobs to different queues, and the worker config decides which queues each process serves. For a small app, one queue is enough.

retry_on Net::ReadTimeout. Active Job’s retry mechanism, with polynomial backoff. On a transient SMTP timeout, retry; after 5 attempts, give up and surface the error. This is the level of retry policy I want most jobs to have — explicit list of expected failures, sane backoff, hard cap on attempts.

discard_on ActiveJob::DeserializationError. If the user got deleted between the time the job was enqueued and the time the worker picked it up, the deserializer can’t find the record. Don’t retry — the user is gone. Discard.

Pass IDs, not records. WelcomeEmailJob.perform_later(user.id), not WelcomeEmailJob.perform_later(user). Active Job can serialize Active Record objects via GlobalID, but the deserializer reloads the record from the database when the job runs — which means stale data, possible record-not-found errors, and an extra query on enqueue. Pass the ID and let the job do the lookup. (If the record is gone by the time the job runs, you’ll get the deserialization error above and discard cleanly.)

6. The dashboard

mission_control-jobs is the official UI for Solid Queue. Add it, mount it in routes behind a Basic Auth or admin guard, and you get the queue-inspector experience Sidekiq users are used to: list of queues, list of in-flight jobs, list of failures, the ability to retry or discard a failed job from a webpage.

# illustrative — config/routes.rb
authenticate :user, ->(u) { u.admin? } do
  mount MissionControl::Jobs::Engine, at: "/jobs"
end

I’d consider this part of the install, not optional. A queue you can’t inspect is a queue you’ll regret the first time something fails.

What you’ve shipped

  • A database-backed Active Job adapter
  • A worker process (or in-dev, a Puma plugin)
  • One example job with retries and a discard rule
  • A web UI for inspection

Total: two commands, one migration, one config tweak, one job class. The Redis dependency is gone; the operational surface is your existing PostgreSQL.

When this isn’t the right swap

To be clear about the tradeoff: Solid Queue uses your application database. Every enqueue is an INSERT, every claim is an UPDATE, every dispatch is a poll. On low-to-medium throughput (low thousands of jobs per minute), this is fine and the simplicity wins. At sustained high throughput (tens of thousands per minute), you’ll want Sidekiq + Redis, or you’ll want Solid Queue on a dedicated database tuned for it. I haven’t benchmarked the exact crossover point, but the rails/solid_queue issue tracker has community reports if you want a sense of where it lives.

For most Rails apps I work on, the throughput never gets there. Solid Queue is the right answer. For the few that do — high-volume notifications, large fanout patterns, real-time pipelines — Sidekiq is still the call.

For everything else, it’s ten minutes.


Reference: rails/solid_queue — MIT, pinned at SHA 176721e33f542e07923fe02964cd55d2c18b4389 for the config example.