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 comparison between Sidekiq, Good Job, and Solid Queue

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 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 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.

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+.

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 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

FeatureSidekiq 8.xGood Job 4.xSolid Queue 1.x
BackendRedis 7.2+PostgreSQLPostgreSQL/MySQL/SQLite
BatchesPro/EnterpriseIncludedPlanned
Unique jobsEnterpriseIncludedManual via locks
Cron schedulingEnterpriseIncludedVia solid_queue_cron
Web UIIncludedIncludedMinimal (Mission Control)
Throughput ceiling100k+ jobs/min10k jobs/min10k jobs/min
Embed in PumaYes (8.x)NoYes
LicenseLGPL + CommercialMITMIT

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

Start 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_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
Daily challenge

Can you spot the bug in Ruby on Rails?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 12, 2026

Tags

#ruby-on-rails
#sidekiq
#good-job
#background-jobs
#active-job

Share

Related articles