Rails Background Jobs: Active Job Vs Sidekiq
Compare Active Job and Sidekiq in Rails: features, performance, reliability, setup, and when to use each for background jobs.
• 19 min read
• 19 min read
Compare Active Job and Sidekiq in Rails: features, performance, reliability, setup, and when to use each for background jobs.
• 19 min read
• 19 min read
Every Rails application eventually hits the same wall: some piece of work is too slow to run inside a web request. Sending a welcome email, resizing an upload, generating a PDF report, charging a customer, syncing with a third-party API. Do any of that synchronously and your users stare at a spinner while a request thread does work that has nothing to do with rendering a response.
That's what background jobs solve, and two names come up constantly: Active Job and Sidekiq. The comparison is a little misleading, though. Active Job is a Rails abstraction, a common interface for defining and enqueuing jobs. Sidekiq is a background job processing system, the thing that actually stores, picks up, and runs jobs. Active Job can use Sidekiq as its backend, which is why people talk about them as competitors when they really operate at different layers of the same problem.
Rails 8 adds another wrinkle: it ships with Solid Queue as the default Active Job backend, a database-backed alternative to Redis-based systems like Sidekiq. So the real question most teams face isn't "Active Job or Sidekiq." It's "which backend should power Active Job, and does my app need Sidekiq specifically."
This article covers what background jobs are, how Active Job and Sidekiq relate, how to wire them together, where Solid Queue fits, and how to make a practical decision for your own app, plus retries, idempotency, testing, monitoring, and the mistakes that show up once jobs hit production. If you're new to the topic, CodeCurious has a beginner's guide to Active Job in Rails worth reading alongside this one.
A Rails web request is supposed to be fast: a user acts, the server works, a response comes back in well under a second. That breaks down when work is slow, like calling a flaky external API or processing a large file.
Synchronous work makes the user wait for everything to finish. Asynchronous work hands the slow part off to run later, outside the request, so the user gets an immediate response while the actual work happens in the background moments afterward.
That handoff relies on a few pieces: a queue holding jobs that are waiting, workers that pull jobs off the queue and run them, execution of the job's code, retries that give a failed job another chance, and scheduling for jobs that shouldn't run immediately.
Here's work that shouldn't run inside a request:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
UserMailer.welcome_email(@user).deliver_later
redirect_to @user, notice: "Welcome!"
else
render :new, status: :unprocessable_entity
end
end
end
deliver_later instead of deliver_now is the difference between the mailer running inline and being handed off as a background job. That's Active Job quietly doing its work.
Active Job is the framework Rails provides for declaring, enqueuing, and configuring background jobs. It isn't a job processor itself. It doesn't run worker processes or manage a queue in Redis or a database. Instead, it defines a consistent Ruby API that sits on top of whichever backend you choose, whether that's Sidekiq, Solid Queue, or something else.
Rails built Active Job to solve fragmentation: every job library used to have its own interface, so switching meant rewriting application code. Active Job standardizes that interface so your job classes look the same no matter what's actually processing them.
Key pieces: job classes inheriting from ApplicationJob with a perform method, enqueuing to schedule a job now or later, adapters connecting Active Job to a specific backend, queue names for routing and prioritization, and declarative retries with retry_on and discard_on.
class WelcomeEmailJob < ApplicationJob
queue_as :default
retry_on Net::SMTPServerBusy, wait: :polynomially_longer, attempts: 5
def perform(user)
UserMailer.welcome_email(user).deliver_now
end
end
ApplicationJob is the base class Rails generates, similar to ApplicationController. It's a natural place for shared configuration every job should inherit.
You enqueue this with WelcomeEmailJob.perform_later(user). That call doesn't run the job immediately; it hands the job and its serialized arguments to whatever adapter you've configured, which decides when and how it actually executes. Your application code talks to a stable API, and you can change the underlying processor without rewriting job classes.
Sidekiq is a background job processing system built for Ruby. Where Active Job is an interface, Sidekiq is an actual engine: it stores jobs, manages worker processes and threads, executes job code, and handles retries and failures.
Sidekiq uses Redis for storage. When a job is enqueued, its arguments are serialized to JSON and pushed onto a Redis list. Sidekiq worker processes poll Redis and execute jobs using a pool of threads, so a single process can run many jobs concurrently without spinning up a full OS process per job, which keeps memory usage relatively efficient.
Sidekiq retries failed jobs with exponential backoff by default; jobs that exhaust their retries land in a dead set you can inspect and manually retry. Sidekiq also ships with a web dashboard showing queue sizes, processing rates, retries, and failures, a big part of its operational reputation.
You can use Sidekiq two ways: directly, writing a Sidekiq worker class against Sidekiq's own API, or through Active Job, configuring Sidekiq as the adapter. Both run jobs through Sidekiq's infrastructure, but they aren't the same thing, as we'll cover shortly.
Active Job and Sidekiq aren't competing alternatives; they operate at different layers. Think of a car: Active Job is the dashboard and steering wheel, a standard interface you use no matter what's under the hood. Sidekiq is one possible engine. Solid Queue is a different engine. The dashboard barely changes; what's producing the power does.
Conceptual flow with Sidekiq as the backend:
Rails Application
→ Active Job (interface)
→ Sidekiq Adapter
→ Sidekiq (processor)
→ Redis (storage)
→ Worker (threads)
→ Job Execution
With Solid Queue instead:
Rails Application
→ Active Job (interface)
→ Solid Queue Adapter
→ Solid Queue (processor)
→ Database (storage)
→ Worker (threads)
→ Job Execution
Same job class, same perform_later call, different engine underneath. Rails also supports other Active Job backends beyond these two.
A common production combination, especially for apps already running Redis for caching or Action Cable.
1. Add the gem:
# Gemfile
gem "sidekiq"
2. Configure the adapter:
# config/application.rb
config.active_job.queue_adapter = :sidekiq
3. Write the job:
class WelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.welcome_email(user).deliver_now
end
end
4. Enqueue it:
WelcomeEmailJob.perform_later(current_user)
5. Start Sidekiq:
bundle exec sidekiq
Once running, Sidekiq connects to Redis, watches the queues Active Job pushes to, and executes perform when the job comes up. Nothing in the job class mentions Sidekiq; the adapter config is the only place that knowledge lives, which is exactly what keeps your business logic backend-agnostic.
Solid Queue is a database-backed background job processor built by the Rails core team, and the default Active Job adapter for new Rails 8 applications. Instead of Redis, it stores jobs and queue state directly in your relational database through ActiveRecord.
That matters because smaller apps don't need a separate Redis instance just to process jobs, and job data lives alongside the rest of your app's data, which can simplify backups and local development.
# config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
Your job classes look identical to the Sidekiq example, since Active Job is the interface either way:
class WelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.welcome_email(user).deliver_now
end
end
Solid Queue suits apps avoiding Redis, simpler infrastructure needs, or job volume that doesn't demand Sidekiq's thread-based concurrency. Teams may still prefer Sidekiq for very high throughput, Sidekiq-specific features like unique jobs, or existing operational experience with Redis at scale. Neither is universally better; the right engine depends on your infrastructure and workload.
You can skip Active Job and write a Sidekiq worker directly:
class WelcomeEmailWorker
include Sidekiq::Job
sidekiq_options queue: "default", retry: 5
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome_email(user).deliver_now
end
end
Enqueued with Sidekiq's own API: WelcomeEmailWorker.perform_async(current_user.id).
Compare that to perform_later(current_user), which lets Active Job serialize the record itself via Global ID instead of requiring you to pass and look up an ID manually.
Practical differences: Sidekiq workers use perform_async/perform_in, while Active Job uses perform_later/set(wait:). Active Job classes can move to a different backend later with minimal change; direct workers are tied to Sidekiq's API. Direct workers expose Sidekiq-specific options like unique jobs or batching that Active Job doesn't fully surface. Active Job ships backend-agnostic test helpers, while testing direct workers means using Sidekiq's own tools. And direct workers couple your logic tightly to Sidekiq, a reasonable trade-off if you're confident you won't switch and need a feature Active Job doesn't expose.
From Active Job: declarative job classes, perform_later/perform_now, delayed execution with .set(wait:), queue naming with queue_as, retry_on/discard_on, argument serialization (including ActiveRecord objects via Global ID), callbacks like before_enqueue, and backend-agnostic testing helpers.
From Sidekiq specifically: Redis-backed storage, thread-based concurrency, the web dashboard, retry exhaustion into a dead set, and advanced options like unique jobs and batches (some require Sidekiq Pro/Enterprise).
It's a common mistake to assume Active Job provides dashboards or concurrency tuning. It doesn't; those belong to whatever backend is processing the jobs. Active Job gives you the vocabulary to define and enqueue work; the backend gives you the execution engine and its tooling.
Performance depends heavily on workload, infrastructure, and job design, not just which library sits underneath Active Job. Throughput and latency are shaped by worker and thread counts, available CPU and memory, how expensive each perform method is, how large job arguments are, and how much load your database and Redis can absorb concurrently.
Redis-backed Sidekiq generally has fast queue operations since Redis is in-memory. Database-backed Solid Queue relies on your database's write and polling performance, fine for moderate volume but a different profile. Neither fact predicts how your app will perform. If throughput matters, benchmark your actual jobs under realistic load rather than trusting generic numbers from someone else's post.
Concurrency means multiple jobs running at once, across processes, threads, or both. Sidekiq typically runs a small number of processes with many threads each; Solid Queue uses a dispatcher and worker processes that can also run multiple threads.
Real considerations: database connection pools, since every thread touching ActiveRecord needs a connection (25 threads against a pool of 5 means checkout timeouts); thread safety, since code mutating shared state can misbehave under parallel execution; external APIs, since concurrent calls hit rate limits faster; and race conditions, when two jobs touch the same record simultaneously without locking.
Getting concurrency right is mostly about configuring your database pool and thread count to match, and writing job logic that tolerates parallel execution, regardless of which tool you chose.
A queue is a named channel jobs sit in until a worker picks them up. Most apps eventually want more than one, so a password reset email doesn't sit behind a slow nightly report.
class PasswordResetJob < ApplicationJob
queue_as :urgent
def perform(user)
UserMailer.password_reset(user).deliver_now
end
end
With Sidekiq, you configure which queues workers watch and their priority in config/sidekiq.yml:
:queues:
- [urgent, 3]
- [default, 2]
- [low, 1]
Solid Queue configures queues similarly through its own configuration tied to worker processes. The concept, prioritizing job types, is the same; the syntax differs because it belongs to the backend, not to Active Job.
Jobs fail: dropped connections, 500s from an API, a briefly unavailable database. Active Job lets you declare retry behavior per exception:
class SyncOrderJob < ApplicationJob
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
discard_on ActiveRecord::RecordNotFound
def perform(order_id)
order = Order.find(order_id)
ExternalOrderSync.call(order)
end
end
retry_on retries with backoff for likely-temporary errors. discard_on gives up immediately for errors retrying won't fix, like a deleted record. Blindly retrying every exception can turn one bad job into repeated wasted work or duplicate side effects.
When retries exhaust, Sidekiq moves jobs into a dead set visible in its dashboard; Solid Queue tracks failures in its own tables. Either way, failed jobs shouldn't disappear silently.
Idempotency means a job produces the same result even if it accidentally runs more than once, and "exactly once" isn't guaranteed by most job systems. A worker can crash after doing the work but before marking it complete, causing a retry to redo it.
Sending an email twice is annoying but survivable. Charging a customer twice is a real problem. Creating a record twice duplicates data. Processing the same webhook twice is common, since many providers redeliver payloads.
Guard against this by checking existing state before acting:
class ChargeCustomerJob < ApplicationJob
def perform(order_id)
order = Order.find(order_id)
return if order.charged?
Payments::Charge.call(order)
order.update!(charged: true)
end
end
Designing for idempotency up front is far easier than debugging duplicate side effects later.
Active Job supports delayed execution natively:
ReminderEmailJob.set(wait: 1.day).perform_later(user)
ReminderEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(user)
This works with any backend supporting scheduled execution, including Sidekiq and Solid Queue.
Recurring, cron-style scheduling isn't part of Active Job itself; that's a backend feature. Sidekiq typically handles recurring jobs through an add-on like sidekiq-cron or its commercial scheduled sets. Solid Queue has its own recurring task configuration. Don't expect cron support inside Active Job's core API; it isn't there.
You need visibility into queue depth, wait times, failures, and job duration once jobs run in production. Sidekiq's dashboard shows queue sizes, throughput, retries, and failed jobs at a glance, a major reason for its operational reputation. With Active Job on a different backend like Solid Queue, lean on that backend's own admin views plus general application monitoring and error trackers like Sentry or Honeybadger.
Regardless of backend, watch queue latency (how long jobs wait before being picked up), failure and retry counts, individual job duration, and overall worker health.
Jobs should fail loudly, not silently.
class SyncInventoryJob < ApplicationJob
retry_on Faraday::TimeoutError, wait: :exponentially_longer, attempts: 3
rescue_from(StandardError) do |exception|
Rails.error.report(exception)
raise exception
end
def perform(product_id)
InventorySync.call(product_id)
end
end
retry_on handles the retryable case; rescue_from reports anything unexpected before re-raising, so the job still shows as failed. That combination gives resilience for temporary issues and visibility for real bugs.
Active Job ships with test helpers that assert on enqueuing behavior without a full backend running:
class WelcomeEmailJobTest < ActiveJob::TestCase
test "enqueues welcome email job" do
user = users(:alice)
assert_enqueued_with(job: WelcomeEmailJob, args: [user]) do
WelcomeEmailJob.perform_later(user)
end
end
test "sends the welcome email when performed" do
user = users(:alice)
perform_enqueued_jobs { WelcomeEmailJob.perform_later(user) }
assert_equal 1, ActionMailer::Base.deliveries.size
end
end
With RSpec, rspec-rails provides matchers that work the same way:
it "enqueues the welcome email job" do
expect { WelcomeEmailJob.perform_later(user) }
.to have_enqueued_job(WelcomeEmailJob).with(user)
end
These test the Active Job layer, verifying your app enqueues the right job with the right arguments, without needing Redis or Sidekiq available in your test suite.
A subtle bug shows up when a job is enqueued inside a transaction and expects a record that hasn't committed.
ActiveRecord::Base.transaction do
order = Order.create!(user: current_user, total: cart.total)
SyncOrderJob.perform_later(order.id) # risky
end
A fast backend might start the job before the transaction commits, so Order.find(order.id) inside perform can raise RecordNotFound even though the record "exists" from the caller's view. Rails mitigates part of this with transactional callbacks that defer enqueuing until after commit, but the risk is worth understanding, especially in older code. When in doubt, enqueue after the transaction closes, or add a small retry for RecordNotFound.
Jobs calling external services need to be defensive: explicit HTTP timeouts so a hung connection doesn't tie up a worker, retries only for genuinely transient errors, backing off after rate-limit responses instead of hammering the API, enough logging to debug failures later, and centrally managed authentication tokens.
class SyncOrderJob < ApplicationJob
retry_on Faraday::TimeoutError, Faraday::ConnectionFailed,
wait: :exponentially_longer, attempts: 5
def perform(order_id)
order = Order.find(order_id)
ExternalOrderSync.call(order, timeout: 10)
end
end
For frequent calls to unreliable third parties, some teams add circuit-breaker logic that stops sending requests to a clearly-down service rather than retrying into a wall, a more advanced pattern worth knowing exists.
Active Job offers a Rails-native API consistent with the rest of the framework, backend flexibility as infrastructure needs change, and easy integration with ActiveRecord, mailers, and Rails testing tools.
Sidekiq offers a mature, production-hardened ecosystem, direct access to Sidekiq-specific features, strong operational tooling through its dashboard, high thread-based concurrency, and a Redis architecture many teams already know.
Neither is objectively better here; it depends on whether you value staying backend-agnostic or want a specific tool's full feature set directly.
Running Sidekiq means operating Redis: provisioning it, monitoring memory, handling persistence, and keeping it available whenever your app enqueues or processes jobs, additional infrastructure beyond your primary database.
Running Solid Queue means your existing database handles job storage, simplifying infrastructure for smaller apps, at the cost of extra read/write load on your primary datastore as volume grows.
Both require planning for worker scaling, memory per worker, safe deploys that don't kill jobs mid-run, and failure recovery. Whichever you choose, set up monitoring around queue depth and failure rates before an incident, not during one.
Use Active Job as the interface when you want a Rails-native abstraction, want to avoid tight coupling to one backend, want flexibility to change backends later, or are using a well-supported backend and don't need backend-specific features.
Reach for Sidekiq specifically when you need Sidekiq-only features, want a mature Redis-backed processor with a long track record, need high thread-based concurrency, or your team already operates Redis comfortably.
Solid Queue may fit better when you want to avoid running Redis, your job volume is moderate, or you're starting fresh on Rails 8 and want the framework default.
In most cases, start with Active Job on whatever backend fits your infrastructure, and reach for backend-specific APIs only when you hit a real limitation.
Rails 8 made Solid Queue the default Active Job backend for new apps, part of a broader push to reduce required infrastructure. That doesn't make Sidekiq obsolete; plenty of production apps with heavy job volume or existing Redis infrastructure still choose it deliberately.
Active Job remains the interface either way. Teams that choose Sidekiq are still typically writing Active Job classes, using perform_later, and relying on its retry and scheduling API, unless they specifically opt into raw Sidekiq workers. Understanding Active Job is foundational for Rails 8 background job architecture regardless of which backend ends up doing the work.
perform.Welcome emails belong in a background job so signup doesn't wait on SMTP; either backend works, with low risk if occasionally duplicated. Image uploads can be CPU-intensive; watch worker memory for large files. PDF reports are often slow and memory-heavy, a good fit for a low-priority queue. Notifications resemble email, but idempotency matters more since push services can redeliver. Payments carry high idempotency stakes; guard against duplicate charges explicitly. Large CSV imports benefit from batching into smaller jobs so a mid-import failure doesn't mean starting over. External API syncs need solid retry and timeout handling so a flaky third party can't back up the whole queue. Monthly reports fit scheduled jobs on a low-priority queue, since timing is predictable and urgency is low.
Active Job's real advantage shows when you need to change backends, say moving from Solid Queue to Sidekiq as volume grows. Since job classes are written against Active Job's API rather than a backend's API, migration often means changing the adapter configuration and infrastructure, not rewriting job classes.
That's not guaranteed, though. Jobs relying on backend-specific behavior, like Sidekiq's sidekiq_options or Solid Queue's database-backed uniqueness guarantees, won't automatically carry over. Test retry behavior, scheduling, and queue configuration thoroughly after switching before trusting the new setup in production.
Is Active Job the same as Sidekiq? No. Active Job is a Rails abstraction for defining and enqueuing jobs; Sidekiq is a processing system that can serve as one of its backends.
Can Active Job use Sidekiq? Yes, by setting config.active_job.queue_adapter = :sidekiq.
Is Sidekiq better than Active Job? They aren't directly comparable since they operate at different layers. The real comparison is Sidekiq versus other Active Job backends like Solid Queue.
Does Rails 8 use Sidekiq by default? No, Rails 8 defaults to Solid Queue.
What is the default Active Job backend in Rails 8? Solid Queue.
Does Sidekiq require Redis? Yes, for queue storage and coordination between workers.
Is Solid Queue better than Sidekiq? Neither is universally better; Solid Queue avoids running Redis, Sidekiq offers a mature, high-concurrency processor with strong tooling. It depends on your infrastructure and volume.
Should I use Active Job or Sidekiq in Rails 8? Use Active Job as the interface regardless; the real decision is which backend to configure.
Can I use Sidekiq without Active Job? Yes, by writing worker classes directly with Sidekiq::Job.
Can I switch from Solid Queue to Sidekiq later? Generally yes, but test retry, scheduling, and backend-specific configuration carefully afterward.
How do I run a Rails background job in production? Run bundle exec sidekiq as a separate process for Sidekiq, or Solid Queue's own worker process, typically managed by your process manager or platform.
How do I retry failed Rails jobs? Declare retry behavior with retry_on, and use your backend's tooling, like Sidekiq's dead set, to inspect and manually retry jobs that already failed.
Active Job and Sidekiq solve different problems. Active Job is the interface Rails gives you for defining and enqueuing work consistently, no matter what actually executes it. Sidekiq is a mature, Redis-backed system that can serve as that backend, or be used directly for its specific features.
Sidekiq remains a strong choice for high concurrency, a proven track record, and Sidekiq-specific capabilities. Active Job earns its place by giving your app a stable, Rails-native job interface that doesn't lock you into one processing engine. Solid Queue, as the Rails 8 default, deserves real consideration if you'd rather skip operating Redis and your volume doesn't demand Sidekiq's concurrency model.
There's no universal winner. The right setup depends on your infrastructure, your team's operational experience, and how much job volume you're actually processing. Start with what fits today, keep job classes written against Active Job's API where you can, and let your production needs, not a generic recommendation, decide which engine sits underneath.