Background Jobs with Active Job & Sidekiq
Reviewed & published by Brayan K
Slow work — sending email, resizing images, calling external APIs — should never block a web request. Background jobs move that work off the request cycle so your app stays fast.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll declare jobs with Active Job, enqueue them with perform_later, pick a backend like Sidekiq, and handle retries, scheduling, and idempotency.
What You'll Learn in This Lesson
- Why long tasks belong off the request cycle
- Declaring jobs with ApplicationJob
- perform_later vs perform_now
- Backends: Sidekiq, Solid Queue, Resque, Delayed Job
- Retries and scheduling
- Writing idempotent jobs that are safe to retry
1️⃣ Declaring and Enqueuing Jobs
An Active Job class inherits from ApplicationJob and does its work in perform. Enqueue it with perform_later so the request returns fast; perform_now runs it inline.
# Active Job gives every job a common shape, whatever backend runs it.
# Generate one with: rails generate job send_welcome_email
class SendWelcomeEmailJob < ApplicationJob
queue_as :default # which queue this job uses
def perform(user_id) # the work to do, given arguments
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end
# In a controller, ENQUEUE it instead of doing the work inline:
class UsersController < ApplicationController
def create
user = User.create!(user_params)
SendWelcomeEmailJob.perform_later(user.id) # async — returns fast
redirect_to user
end
end
# perform_later -> enqueue, run later by a worker
# perform_now -> run immediately, in this process (rarely in web)2️⃣ Backends, Scheduling, and Retries
Active Job is the interface; a backend actually runs the jobs. Configure one adapter (:sidekiq, :solid_queue, and others), then use scheduling and retry declarations.
# Active Job is just the INTERFACE; a backend actually runs jobs.
# Configure one in config/application.rb:
config.active_job.queue_adapter = :sidekiq # uses Redis
# Other choices: :solid_queue (database), :resque, :delayed_job,
# :async (in-process, dev only)
# Scheduling and retries are first-class:
ReportJob.set(wait: 1.hour).perform_later # run in an hour
ReportJob.set(wait_until: Date.tomorrow.noon).perform_later
class FlakyApiJob < ApplicationJob
retry_on Net::OpenTimeout, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError # give up cleanly
def perform; end
end
# Start a Sidekiq worker process with: bundle exec sidekiqYour turn. Finish the resize job's perform body, then enqueue it asynchronously.
# 🎯 YOUR TURN — finish the job, then enqueue it.
class ResizeAvatarJob < ApplicationJob
queue_as :images
def perform(user_id)
user = User.find(user_id)
# 👉 TODO: do the slow work here (e.g. resize the image)
user.avatar.resize!("128x128")
end
end
# Enqueue it asynchronously so the web request stays fast:
ResizeAvatarJob.perform_later(user.id)
# ✅ Expected log line:
# Enqueued ResizeAvatarJob to ...(images) with arguments: <user_id>Mini-Challenge: An Idempotent Job
Design a scheduled NightlyReportJob that is safe to retry — re-running it for the same date must not create duplicates.
# 🎯 MINI-CHALLENGE: design an idempotent job.
# Sketch (no need to run) a NightlyReportJob that:
#
# 1. Inherits from ApplicationJob and uses queue_as :reports.
# 2. Takes a date argument in perform(date).
# 3. Is IDEMPOTENT — guard so re-running for the same date does not
# create a duplicate report (e.g. find_or_create_by).
# 4. Is scheduled with: NightlyReportJob.set(wait_until: ...).perform_later
#
# ✅ Goal: safe to retry, runs off the request cycle.
# your code hereCommon Errors (and the fix)
- Calling perform_now in a controller — That blocks the request; use perform_later to enqueue.
- Passing whole objects as arguments — Pass an id like user.id and re-find it inside perform.
- No worker running — With Sidekiq you must start bundle exec sidekiq or jobs sit in the queue.
- Non-idempotent jobs — Retries can double-charge or double-send; guard side effects.
- Forgetting to configure the adapter — Set config.active_job.queue_adapter or jobs run in-process only.
Pro Tips
- 💡 Keep perform small: fetch by id, do one job of work, and finish quickly.
- 💡 Use queues: queue_as separates urgent jobs from slow batch work.
- 💡 Monitor: Sidekiq ships a web dashboard to watch queues, retries, and failures.
📋 Quick Reference — Background Jobs
| Concept | Rails Syntax |
|---|---|
| Define a job | class X < ApplicationJob |
| Enqueue (async) | X.perform_later(id) |
| Run now (sync) | X.perform_now(id) |
| Schedule | X.set(wait: 1.hour) |
| Retry on error | retry_on SomeError |
| Choose queue | queue_as :default |
🎉 Lesson Complete!
- ✅ Long tasks belong off the request cycle
- ✅ Jobs inherit from ApplicationJob and do work in perform
- ✅ perform_later enqueues; perform_now runs inline
- ✅ Backends like Sidekiq and Solid Queue actually run jobs
- ✅ Make jobs idempotent so retries are safe
Practice quiz
Why move slow work into a background job?
- to keep the web request fast by doing the work asynchronously
- to avoid writing tests
- to bypass routing
- to make the database larger
Answer: to keep the web request fast by doing the work asynchronously. Long tasks like email or image processing run off the request cycle so the user gets a fast response.
What is Active Job in Rails?
- a database adapter
- a templating engine
- a framework for declaring jobs and running them on various backends
- a gem for CSS
Answer: a framework for declaring jobs and running them on various backends. Active Job is a Rails framework that provides a common interface for background jobs across backends.
Which class do Active Job jobs typically inherit from?
- ActionController::Base
- ApplicationJob
- ActiveJob::Runner
- ApplicationRecord
Answer: ApplicationJob. Generated jobs inherit from ApplicationJob, which inherits from ActiveJob::Base.
Which method enqueues a job to run asynchronously?
- perform_now
- call_async
- run_background
- perform_later
Answer: perform_later. perform_later enqueues the job; perform_now would run it immediately in the current process.
What does perform_now do?
- runs the job immediately in the current process
- retries the job
- schedules the job for tomorrow
- deletes the job
Answer: runs the job immediately in the current process. perform_now executes the job synchronously right away rather than enqueuing it.
Which popular backend uses Redis to store and process jobs?
- Nginx
- Sidekiq
- SQLite
- Memcached
Answer: Sidekiq. Sidekiq is a widely used background processing library that relies on Redis.
Which newer backend stores jobs in your relational database (no Redis)?
- Resque
- Delayed Job classic
- Faktory
- Solid Queue
Answer: Solid Queue. Solid Queue is a database-backed Active Job backend, now a Rails default.
What does it mean for a job to be idempotent?
- it never fails
- it ignores arguments
- running it more than once has the same effect as running it once
- it can only run once ever
Answer: running it more than once has the same effect as running it once. An idempotent job is safe to retry because repeating it does not cause duplicate side effects.
How do you schedule a job to run later with Active Job?
- MyJob.delay(1.hour)
- MyJob.set(wait: 1.hour).perform_later
- MyJob.sleep(1.hour)
- MyJob.after(1.hour)
Answer: MyJob.set(wait: 1.hour).perform_later. set(wait: 1.hour).perform_later (or wait_until:) schedules the job for the future.
Why do background backends retry failed jobs automatically?
- to recover from transient errors like a brief network outage
- to skip validations
- to slow the app down
- to delete the queue
Answer: to recover from transient errors like a brief network outage. Automatic retries help jobs survive temporary failures; idempotency makes those retries safe.
Continue this course
- Previous: Sinatra & Rack
- Next: Hotwire, Turbo & Action Cable — SPA-like Rails with Turbo Drive/Frames/Streams, Stimulus and Action Cable
- Quick reference: Ruby cheat sheet
Frequently asked questions
Why should long tasks run off the web request cycle?
Web requests should return quickly so the user is not left staring at a spinner, and so your server can handle many requests at once. Tasks like sending email, generating PDFs, processing images, calling slow third-party APIs, or crunching reports can take seconds or minutes — far too long to block a request. By enqueuing them as background jobs, the controller finishes immediately and a separate worker process does the heavy lifting later. This keeps the app responsive, prevents request timeouts, and lets you scale workers independently of web servers when the workload grows.
What does Active Job actually do?
Active Job is the Rails framework that gives background jobs a single, consistent interface regardless of which queuing backend you use. You write a class that inherits from ApplicationJob with a perform method, then enqueue it with perform_later. Active Job handles serializing arguments, choosing a queue with queue_as, scheduling with set(wait:), and declaring retries with retry_on or discard_on. The key benefit is portability: the same job code runs on Sidekiq, Solid Queue, Resque, or others, so you can switch backends by changing one configuration line rather than rewriting every job.
Which backend should I choose: Sidekiq, Solid Queue, or others?
Sidekiq is the long-standing favorite for high throughput; it stores jobs in Redis and processes them efficiently with threads, and has a rich ecosystem and dashboard. Solid Queue is a newer, database-backed backend that became a Rails default, attractive because it needs no extra Redis service. Resque and Delayed Job are older options still used in many codebases; Delayed Job is database-backed and simple, while Resque also uses Redis. The right pick depends on scale and infrastructure: if you already run Redis and need high volume, Sidekiq is excellent; if you want fewer moving parts, Solid Queue keeps everything in your existing database.
What is idempotency and why do jobs need it?
An idempotent job produces the same result whether it runs once or several times, so re-running it causes no extra side effects. This matters because background systems retry failed jobs automatically — to recover from transient problems like a brief network outage — and a job can occasionally run more than once even after appearing to succeed. If a payment or email job is not idempotent, a retry could charge a card twice or send duplicate messages. You make jobs idempotent by guarding their effects: check whether the work is already done, use unique constraints or find_or_create_by, and design operations so repeating them is safe.