RoR ActiveJob
ActiveJob is Rails' unified interface for background jobs. It sits between your application code and whatever job backend you use — Sidekiq, Resque, Delayed Job, or others. You write one job class using ActiveJob, and you can swap the backend without changing your job code. Think of ActiveJob as the adapter layer that makes background jobs portable.
ActiveJob's Role in the Stack
Your Job Code (ApplicationJob subclass)
|
v
ActiveJob
(unified interface)
|
v
Backend Adapter
|
+---------+---------+
| | |
Sidekiq Resque Delayed Job
(Redis) (Redis) (Database)
You write your job once. Switch backends by changing one line in application.rb.
Creating a Job
rails generate job CleanupOldRecords
app/jobs/cleanup_old_records_job.rb
class CleanupOldRecordsJob < ApplicationJob
queue_as :maintenance
def perform
cutoff = 6.months.ago
expired_sessions = Session.where("created_at < ?", cutoff)
count = expired_sessions.count
expired_sessions.delete_all
Rails.logger.info "Cleaned up #{count} expired sessions."
end
end
Enqueue Syntax
Run as soon as possible: CleanupOldRecordsJob.perform_later Run with arguments: ProcessOrderJob.perform_later(order.id, "priority") SendReminderJob.perform_later(user.id) Run after a delay: WelcomeEmailJob.set(wait: 5.minutes).perform_later(user.id) Run at a specific time: RenewalReminderJob.set(wait_until: subscription.expires_at - 3.days).perform_later(sub.id) Run synchronously (for testing / scripts): CleanupOldRecordsJob.perform_now
Passing Arguments
ActiveJob serializes arguments automatically. You can pass basic Ruby types and ActiveRecord objects:
Supported argument types: Integer, Float, String, Symbol Array, Hash true, false, nil Date, Time, DateTime ActiveRecord model instances (serialized as GlobalID)
Passing an ActiveRecord object:
NotifyUserJob.perform_later(user)
Inside the job:
def perform(user)
UserMailer.notification(user).deliver_now
end
ActiveJob serializes the user as a GlobalID reference. When the job runs, it fetches the latest version of the user from the database. If the user was deleted in the meantime, the job raises ActiveRecord::RecordNotFound.
ApplicationJob — Shared Configuration
app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError
before_enqueue do |job|
Rails.logger.info "Enqueuing #{job.class.name} with args: #{job.arguments}"
end
after_perform do |job|
Rails.logger.info "Completed #{job.class.name}"
end
end
All your jobs inherit from ApplicationJob. Put shared retry logic, logging, and error handling here instead of in every individual job.
Job Callbacks
class ImportDataJob < ApplicationJob
before_enqueue { Rails.logger.info "Job about to be queued" }
after_enqueue { Rails.logger.info "Job successfully queued" }
before_perform { Rails.logger.info "Job starting" }
after_perform { Rails.logger.info "Job finished" }
around_perform do |job, block|
start = Time.now
block.call
duration = Time.now - start
Rails.logger.info "Job took #{duration.round(2)}s"
end
def perform(import_file_path)
DataImporter.new(import_file_path).run
end
end
Error Handling
class SyncInventoryJob < ApplicationJob
queue_as :default
retry_on InventoryService::Timeout, wait: 30.seconds, attempts: 5
retry_on Net::OpenTimeout, wait: 1.minute, attempts: 3
discard_on ActiveRecord::RecordNotFound
def perform(product_id)
product = Product.find(product_id)
InventoryService.sync(product)
rescue InventoryService::Error => e
Rails.logger.error "Inventory sync failed for product #{product_id}: #{e.message}"
raise ← re-raise so ActiveJob triggers the retry
end
end
Queues
Organize jobs into queues based on priority and resource needs:
class PaymentJob < ApplicationJob queue_as :critical ← process fast, highest priority end class WelcomeEmailJob < ApplicationJob queue_as :mailers ← standard priority end class ReportJob < ApplicationJob queue_as :reports ← can wait, resource intensive end class ArchiveJob < ApplicationJob queue_as :low ← lowest priority, non-urgent end
Testing ActiveJob
spec/jobs/send_welcome_email_job_spec.rb
require "rails_helper"
RSpec.describe SendWelcomeEmailJob, type: :job do
include ActiveJob::TestHelper
let(:user) { create(:user) }
describe "#perform" do
it "sends a welcome email" do
expect {
described_class.perform_now(user.id)
}.to have_enqueued_mail(UserMailer, :welcome_email)
end
end
describe "enqueuing" do
it "enqueues the job in the default queue" do
expect {
SendWelcomeEmailJob.perform_later(user.id)
}.to have_enqueued_job(SendWelcomeEmailJob)
.with(user.id)
.on_queue("default")
end
end
end
ActiveJob with Sidekiq vs Inline Adapter
| Adapter | Config | When to Use |
|---|---|---|
| :async | Default in development | Dev only — runs in process threads |
| :inline | config.active_job.queue_adapter = :inline | Test environments — runs jobs immediately |
| :sidekiq | gem "sidekiq" | Production — persistent, reliable, fast |
| :resque | gem "resque" | Alternative to Sidekiq |
| :delayed_job | gem "delayed_job_active_record" | Simple setups — stores jobs in DB |
ActiveJob lets you build background job logic once and adapt it to any backend. Start with the async adapter in development, use inline for tests, and switch to Sidekiq for production without touching a single line of your job code.
