ActiveJob and Sidekiq: background jobs in Rails
ActiveJob and Sidekiq are a common pairing in Rails applications that need background work. ActiveJob gives you a Rails-friendly job API, while Sidekiq does the actual processing with Redis and worker processes. Together, they let your app handle slow work outside the request cycle.
Intro context
Background jobs are not just for big applications. The moment your app sends an email, resizes an image, talks to an API, or generates a report, you have work that should happen after the user gets a response. ActiveJob gives that work a standard shape, and Sidekiq gives it a fast, production-ready queue.
This tutorial focuses on the practical flow. You will see how to install Sidekiq, create a job, enqueue it, monitor it, and choose settings that make the queue easier to reason about later.
TL;DR
- Use ActiveJob for the job interface and Sidekiq as the backend.
- Keep jobs small, serializable, and easy to retry.
- Use Redis for the queue and Sidekiq Web for visibility.
- Put slow or fragile work into jobs so web requests stay responsive.
- Add retries, queue priorities, and monitoring early, not after the queue is already noisy.
What are ActiveJob and Sidekiq?
ActiveJob is Rails’ abstraction for background jobs. It gives you one API, even if you change queue backends later. Sidekiq is a worker system that pulls jobs from Redis and runs them in the background.
That split is useful because your application code stays focused on intent, while Sidekiq handles execution details. In practice, you write a job class once, then choose the backend in configuration.
class ProcessReportJob < ApplicationJob
queue_as :default
def perform(report_id)
report = Report.find(report_id)
report.process!
end
end
The job class reads like ordinary Ruby, which is the point. The framework does the queuing work, while your code stays focused on the business action.
Setting up Sidekiq
Add Sidekiq and Redis to your Gemfile, then install the Sidekiq configuration that Rails expects.
gem "sidekiq"
gem "redis"
The job class reads like ordinary Ruby, which is the point. ActiveJob gives you a standard interface while Sidekiq handles the execution details underneath. In practice, you write a job class once using the ActiveJob DSL, then choose Sidekiq or another backend in configuration without changing the job code.
rails g sidekiq:install
That generator creates the initializer and queue configuration you need to get started. You still need Redis running, because Sidekiq stores jobs in Redis before workers consume them.
Sidekiq works by storing jobs as JSON payloads in Redis, then having worker processes pull those jobs and execute them. That means arguments must be serializable. If you pass a full Active Record object instead of an ID, the object may be stale by the time the job runs because the database could have changed between enqueue time and execution time. Passing an ID and reloading is safer.
:concurrency: 5
:queues:
- default
- mailers
- [critical, 3]
This sample config gives the worker a few queues to process. The critical queue gets more weight, which is useful when some jobs matter more than others.
Creating your first job
Generating a job gives you a clear place to put the work that should happen asynchronously.
rails g job ProcessReport
Rails creates a job class in app/jobs/process_report_job.rb. A small job is usually better than a large one, because small jobs are easier to retry and easier to test.
Retries are important because transient failures happen in real systems. A network timeout or a briefly unavailable database should not mean permanent data loss. Setting retries with exponential backoff gives the system time to recover before giving up, which is why this pattern shows up in nearly every production job class.
class ProcessReportJob < ApplicationJob
queue_as :default
def perform(report_id)
report = Report.find(report_id)
report.process!
ReportMailer.completed(report).deliver_later
end
end
Notice that the job takes an ID instead of a full object. That keeps the payload small and makes the job safer to serialize.
Enqueuing jobs
Once the job exists, you can enqueue it in a few different ways depending on how urgent the work is.
# Run as soon as a worker is available
ProcessReportJob.perform_later(report.id)
# Run after a delay
ProcessReportJob.perform_in(1.hour, report.id)
# Run later if you want a specific future time
ProcessReportJob.set(wait_until: tomorrow.midnight).perform_later(report.id)
The important idea is that the request only schedules the work. The worker does the actual execution later, which keeps your web response fast and predictable.
ActiveJob is also what powers deliver_later on mailers. That means your email delivery follows the same queueing path as your own jobs, which makes the system easier to observe and tune.
Job execution options
Sidekiq and ActiveJob both give you a few levers for reliability. Retries are especially important, because transient failures happen in real systems.
class MyJob < ApplicationJob
queue_as :default
retry_on StandardError, wait: :exponentially_longer, attempts: 5
discard_on ActiveJob::DeserializationError
def perform(*args)
# Job logic goes here
end
end
retry_on handles temporary failures. discard_on is for exceptions that should not be retried, such as a job argument that can no longer be deserialized. The right choice depends on whether repeating the job can realistically fix the problem.
Queue priority
You can route important work to a priority queue when you enqueue the job.
MyJob.set(queue: :critical).perform_later(args)
That is helpful when some work must move faster than the rest. For example, a password reset email should probably beat a low-priority analytics export.
Monitoring with the web UI
Sidekiq includes a web interface that shows queues, retries, failures, and scheduled jobs. Mount it in your routes so you can inspect what the workers are doing.
require "sidekiq/web"
Rails.application.routes.draw do
mount Sidekiq::Web => "/sidekiq"
end
The web UI matters because background jobs can fail quietly if nobody looks at the queue. A dashboard gives you a quick way to see whether jobs are flowing, backing up, or failing in a repeatable way.
Common panels include the live queues, retry list, scheduled jobs, and dead jobs. When a job is retrying more than expected, the UI helps you tell whether you have a bad payload, a flaky dependency, or a genuine application bug.
Best practices
Keep jobs focused
Each job should do one thing. If a workflow has several steps, split them into separate jobs so one failure does not block everything else.
class SendWelcomeEmailJob < ApplicationJob; end
class SetupDefaultPreferencesJob < ApplicationJob; end
class NotifyAdminOfSignupJob < ApplicationJob; end
Smaller jobs are easier to retry, easier to test, and easier to understand later when you read the queue history.
Pass IDs, not full objects
Jobs should store serializable values such as IDs. That keeps the payload small and lets the job load fresh data when it starts.
WelcomeMailerJob.perform_later(user.id)
If you pass a whole Active Record object, it may be stale by the time the job runs. IDs avoid that problem and keep the queue safer.
Add logging around important work
You can use callbacks or helper methods to track how long a job takes. That gives you more information when a queue slows down.
class MyJob < ApplicationJob
around_perform :measure_time
private
def measure_time
start = Time.current
yield
Rails.logger.info "Job took #{Time.current - start} seconds"
end
end
The extra logging is helpful when you need to tell whether a slow queue is caused by one expensive job or many smaller ones.
Set realistic limits
Long jobs are not automatically bad, but they should be deliberate. If a task takes minutes instead of seconds, document that choice and give it a queue that does not block the rest of the system.
class LongRunningJob < ApplicationJob
sidekiq_options timeout: 300
def perform
# This job is allowed to run for five minutes.
end
end
Error handling
Retries are one of the biggest reasons to use Sidekiq. A short network failure should not mean permanent data loss.
Smaller jobs are easier to retry, easier to test, and easier to understand later when you read the queue history. If a workflow has several steps, split them into separate jobs so one failure does not block everything else. The welcome email, preference setup, and admin notification can each run independently and fail independently.
class MyJob < ApplicationJob
retry_on StandardError, wait: :exponentially_longer, attempts: 5
end
For jobs that must not run twice, consider idempotency and unique job settings. That way, a retry does not create duplicate side effects such as two welcome emails or two charges.
The extra timing information is helpful when you need to tell whether a slow queue is caused by one expensive job or many smaller ones queued behind it. Logging around important work with around_perform gives you a lightweight performance baseline without adding overhead to every job in the system.
class SendWelcomeEmailJob < ApplicationJob
sidekiq_options unique_for: 1.hour
def perform(user_id)
# Prevent duplicate work while the job is still relevant.
end
end
The safest jobs can be run again without changing the result. If you build with that rule in mind, retries become much easier to trust.
Running Sidekiq
Once the app is configured, start the worker process with Bundler so it uses the same bundle as your Rails app.
bundle exec sidekiq
If you use Docker, systemd, or a process manager, the command is still the same. Only the runtime wrapper changes. The goal is to keep the worker alive separately from the web process so each can scale on its own.
Frequently asked questions
When should I use ActiveJob instead of calling Sidekiq directly?
Use ActiveJob when you want your job code to stay portable and fit the Rails conventions. It keeps the app easier to read and makes backend changes less painful.
How do I know whether a job belongs in the default queue?
Put routine background work in the default queue. Use a dedicated queue when the job is slow, urgent, or likely to need different retry settings.
Why do jobs use IDs instead of full objects?
IDs are smaller, safer to serialize, and more likely to point to fresh data when the job actually runs.
What should I monitor first?
Start with queue depth, retry count, and failure count. Those three numbers tell you whether the system is healthy or starting to fall behind.
Summary
ActiveJob gives Rails a clean job interface, and Sidekiq gives that interface a fast queue backed by Redis. Together they let you move slow or fragile work out of the request cycle without losing clarity in your application code.
The most important habits are simple: keep jobs small, pass IDs instead of full objects, add retries where failure is temporary, and watch the queue so problems are visible early.
Next steps
Now that you can process background jobs with ActiveJob and Sidekiq, the next layer to understand is how Rails processes requests before they reach your controllers. The Rails middleware tutorial shows how the middleware stack wraps every request, and the Rails concerns and service objects tutorial covers organizing business logic that often gets triggered by background jobs.
See Also
- Rails routing - useful context for the request cycle before jobs run in the background
- ActiveRecord Basics - jobs often load records and update them in the database
- Ruby File I/O - helpful when background jobs need to read or write files
- Sidekiq official docs - primary documentation for queue configuration and monitoring