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.

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.
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.
# 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
endThe 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 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).
# 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') }
endSidekiq 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 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.
# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3]
- [default, 2]
- [low, 1]
# Capsules for isolated processing
:capsules:
imports:
:concurrency: 2
:queues:
- importsThe embedding API also lets Sidekiq run inside the Puma process for small applications, similar to how Solid Queue's Puma plugin works.
Ready to ace your Ruby on Rails interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Good Job 4.x: PostgreSQL-Native Batches and Uniqueness
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+.
# 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
endGood 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
# 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])
endUnique Jobs for Idempotency
# 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
endThe 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 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.
# config/solid_queue.yml
production:
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 5
polling_interval: 0.1Solid 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.
# 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
endQ2: 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:
# Correct pattern for Sidekiq
class Order < ApplicationRecord
after_commit :enqueue_confirmation, on: :create
private
def enqueue_confirmation
OrderConfirmationJob.perform_later(id)
end
endQ3: 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.
# 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
)
endQ5: 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:
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
endStart practicing!
Test your knowledge with our interview simulators and technical tests.
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_commitcallbacks 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
Can you spot the bug in Ruby on Rails?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 12, 2026
Tags
Share
Related articles

Solid Queue and Solid Cache in Rails 8: Complete Guide for Technical Interviews 2026
Deep dive into Solid Queue and Solid Cache, the database-backed defaults in Rails 8. Architecture, configuration, concurrency controls, and interview-ready knowledge for 2026.

Rails Active Storage in 2026: File Uploads, S3 Integration and Interview Questions
Master Rails Active Storage for file uploads with S3 and direct uploads. Complete tutorial with code examples and common interview questions about file handling in Ruby on Rails.

Rails Service Objects in 2026: Design Patterns, PORO and Technical Interview Questions
Master Rails service objects with PORO patterns, Result monads, and clean architecture. Includes real interview questions and production-ready code examples for Rails 8.