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

AdapterConfigWhen to Use
:asyncDefault in developmentDev only — runs in process threads
:inlineconfig.active_job.queue_adapter = :inlineTest environments — runs jobs immediately
:sidekiqgem "sidekiq"Production — persistent, reliable, fast
:resquegem "resque"Alternative to Sidekiq
:delayed_jobgem "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.

Leave a Comment

Your email address will not be published. Required fields are marked *