# Rails Background Jobs in 2026: Sidekiq vs Good Job and Interview Questions > Compare Sidekiq, Good Job, and Solid Queue for Rails background jobs. Learn when to choose each option and prepare for technical interview questions about Active Job. - Published: 2026-09-12 - Updated: 2026-09-12 - Author: Anthony Fillion-Maillet - Tags: ruby-on-rails, sidekiq, good-job, background-jobs, active-job - Reading time: 5 min --- Rails background jobs have evolved significantly with Rails 8.x and the introduction of Solid Queue as the default Active Job backend. Choosing between Sidekiq, Good Job, and Solid Queue depends on throughput requirements, infrastructure constraints, and the features each project needs. > **Quick Decision Guide** > > For new Rails 8 projects, start with Solid Queue. Switch to Sidekiq for high-throughput workloads exceeding 100,000 jobs/day or when Redis-level latency matters. Choose Good Job for PostgreSQL-backed jobs with batches and unique job constraints out of the box. ## How Active Job Unifies Background Processing Active Job provides a standardized interface for declaring, queuing, and executing background work in Rails. The framework abstracts away the differences between queue backends, allowing code to remain portable across Sidekiq, Good Job, Solid Queue, or any other adapter. ```ruby # app/jobs/payment_processor_job.rb class PaymentProcessorJob < ApplicationJob queue_as :critical retry_on Stripe::RateLimitError, wait: :polynomially_longer, attempts: 5 discard_on Stripe::InvalidRequestError def perform(order_id) order = Order.find(order_id) PaymentService.process(order) OrderMailer.confirmation(order).deliver_later end end ``` The `retry_on` and `discard_on` callbacks handle transient failures and permanent errors without backend-specific code. The `queue_as` directive assigns priority, which each backend interprets according to its own queue processing strategy. ## Sidekiq 8.x: Redis-Backed Throughput [Sidekiq](https://github.com/sidekiq/sidekiq) remains the performance baseline for Rails background jobs. The 8.x release series requires Ruby 3.2+, Rails 7.0+, and Redis 7.2+ (Valkey and Dragonfly work as drop-in replacements since Redis changed its license). ```ruby # config/initializers/sidekiq.rb Sidekiq.configure_server do |config| config.redis = { url: ENV.fetch('REDIS_URL') } config.concurrency = 10 end Sidekiq.configure_client do |config| config.redis = { url: ENV.fetch('REDIS_URL') } end ``` Sidekiq 8 brought three headline changes: a Web UI rewritten from scratch reducing CSS from 160KB to 16KB and average page render time from 55ms to 3ms, job profiling with [Vernier](https://github.com/jhawthorn/vernier) behind a new Profiles tab, and job metrics retained for up to 72 hours. ### When Sidekiq Makes Sense Sidekiq fits workloads where sustained throughput, sub-millisecond job pickup latency, or enterprise features like rate limiting and unique jobs justify running Redis. Capsules, introduced in 7.0 and now standard, allow one Sidekiq process to run multiple isolated thread pools with their own concurrency and queues. ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] - [default, 2] - [low, 1] # Capsules for isolated processing :capsules: imports: :concurrency: 2 :queues: - imports ``` The embedding API also lets Sidekiq run inside the Puma process for small applications, similar to how Solid Queue's Puma plugin works. ## Good Job 4.x: PostgreSQL-Native Batches and Uniqueness [Good Job](https://github.com/bensheldon/good_job) uses PostgreSQL's LISTEN/NOTIFY for job pickup and advisory locks for run-once safety. Version 4.19.x (current as of September 2026) supports Rails 6.1+ and Ruby 3.0+. ```ruby # config/application.rb config.active_job.queue_adapter = :good_job # config/initializers/good_job.rb Rails.application.configure do config.good_job.execution_mode = :async config.good_job.max_threads = 5 config.good_job.poll_interval = 5 config.good_job.shutdown_timeout = 25 end ``` Good Job ships batches and unique jobs in the open-source tier, features that Sidekiq reserves for paid licenses. The `perform_later` call inserts a row into the `good_jobs` table within the same database transaction as the calling code, so if the transaction rolls back, the job never gets created. ### Batches Without a License ```ruby # app/jobs/batch_import_job.rb class BatchImportJob < ApplicationJob def perform(batch, params) batch.on(:finish, ImportCompletionJob, params) params[:file_ids].each do |file_id| batch.add do ProcessFileJob.perform_later(file_id) end end end end # Enqueue the batch GoodJob::Batch.enqueue do |batch| BatchImportJob.perform_later(batch, file_ids: [1, 2, 3]) end ``` ### Unique Jobs for Idempotency ```ruby # app/jobs/sync_inventory_job.rb class SyncInventoryJob < ApplicationJob include GoodJob::ActiveJobExtensions::UniqueJob good_job_control_concurrency_with( perform_limit: 1, key: -> { "sync_inventory_#{arguments.first}" }, duration: 1.hour ) def perform(warehouse_id) InventorySync.run(warehouse_id) end end ``` The `perform_limit: 1` constraint ensures only one sync runs per warehouse at any time, preventing duplicate work when the same job gets enqueued multiple times. ## Solid Queue: The Rails 8 Default [Solid Queue](https://github.com/rails/solid_queue) stores jobs in the application database using `FOR UPDATE SKIP LOCKED`, a PostgreSQL and MySQL feature that turns a regular table into a job queue. New Rails 8 applications ship with Solid Queue configured out of the box. ```ruby # config/solid_queue.yml production: dispatchers: - polling_interval: 1 batch_size: 500 workers: - queues: "*" threads: 5 polling_interval: 0.1 ``` Solid Queue can run inside the Puma web server process with `SOLID_QUEUE_IN_PUMA=true`, which simplifies deployment for applications that do not need dedicated worker processes. Jobs persist in database tables, surviving process restarts and deployments without external dependencies. ## Feature Comparison Table | Feature | Sidekiq 8.x | Good Job 4.x | Solid Queue 1.x | |---------|-------------|--------------|------------------| | Backend | Redis 7.2+ | PostgreSQL | PostgreSQL/MySQL/SQLite | | Batches | Pro/Enterprise | Included | Planned | | Unique jobs | Enterprise | Included | Manual via locks | | Cron scheduling | Enterprise | Included | Via solid_queue_cron | | Web UI | Included | Included | Minimal (Mission Control) | | Throughput ceiling | 100k+ jobs/min | 10k jobs/min | 10k jobs/min | | Embed in Puma | Yes (8.x) | No | Yes | | License | LGPL + Commercial | MIT | MIT | ## Interview Questions on Rails Background Jobs Technical interviews often probe understanding of job reliability, retry strategies, and architecture decisions. Below are questions that distinguish experienced candidates. ### Q1: How does `retry_on` differ from backend-specific retry logic? `retry_on` is an Active Job callback that works across all backends. It catches specific exceptions, applies a wait strategy (`:polynomially_longer` backs off exponentially), and re-enqueues the job. Backend-specific retry logic, like Sidekiq's `sidekiq_retry_in`, only works with that backend and can override Active Job's behavior. ```ruby # Active Job retry (portable) class ApiSyncJob < ApplicationJob retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 8 def perform(record_id) ExternalApi.sync(record_id) end end ``` ### Q2: What happens to a job if the database transaction that enqueued it rolls back? With database-backed queues (Good Job, Solid Queue), the job row is part of the transaction, so it rolls back with everything else. With Redis-backed queues (Sidekiq), the job is already in Redis before the transaction completes, creating orphaned jobs. The workaround is `after_commit` callbacks: ```ruby # Correct pattern for Sidekiq class Order < ApplicationRecord after_commit :enqueue_confirmation, on: :create private def enqueue_confirmation OrderConfirmationJob.perform_later(id) end end ``` ### Q3: When would Good Job outperform Sidekiq? Good Job outperforms Sidekiq when the operational cost of Redis exceeds its throughput benefits. For applications processing fewer than 50,000 jobs/day, PostgreSQL-backed queues avoid the infrastructure, monitoring, and failover complexity of a separate Redis cluster. Good Job also provides batches and unique jobs without license fees, which matters for teams that need those features on a budget. ### Q4: Explain job idempotency and why it matters for retries Idempotency means running the same job multiple times produces the same result as running it once. Jobs must be idempotent because network failures, process crashes, or timeout handling can cause duplicate executions. Techniques include unique constraints in the database, checking for existing results before processing, and using external idempotency keys for payment APIs. ```ruby # Idempotent payment processing def perform(order_id, idempotency_key) return if Payment.exists?(idempotency_key: idempotency_key) Payment.create!( order_id: order_id, idempotency_key: idempotency_key, amount: Order.find(order_id).total ) end ``` ### Q5: How do you handle jobs that must run exactly once across distributed workers? Distributed locks prevent concurrent execution. Good Job uses PostgreSQL advisory locks. Sidekiq Enterprise offers unique jobs. For manual implementation, use Redis `SETNX` or PostgreSQL `pg_try_advisory_lock`: ```ruby class ExclusiveJob < ApplicationJob def perform(resource_id) lock_key = "exclusive_job:#{resource_id}" ActiveRecord::Base.connection.execute( "SELECT pg_try_advisory_lock(hashtext('#{lock_key}'))" ).first['pg_try_advisory_lock'] or return begin process_resource(resource_id) ensure ActiveRecord::Base.connection.execute( "SELECT pg_advisory_unlock(hashtext('#{lock_key}'))" ) end end end ``` ## Choosing the Right Backend for Rails Background Jobs - Start with Solid Queue for new Rails 8 projects that do not yet have Redis in the stack - Switch to Sidekiq when job volume exceeds 50,000/day or pickup latency below 100ms matters - Choose Good Job for PostgreSQL-only deployments that need batches, unique jobs, or cron scheduling without license costs - Use `after_commit` callbacks when enqueuing jobs with Sidekiq to avoid orphaned jobs on transaction rollback - Make every job idempotent because retries and distributed processing can cause duplicate executions - For interviews, demonstrate understanding of transactional job enqueuing, retry strategies, and distributed locking patterns --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ruby-on-rails/rails-background-jobs-sidekiq-good-job-comparison