active-job

Installation
SKILL.md

Active Job

Comprehensive guide to background job processing in Rails with Active Job.

Creating Jobs

rails generate job ProcessOrder
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
  queue_as :default

  def perform(order)
    # Process the order
    order.process!
    OrderMailer.confirmation(order).deliver_now
  end
end

Enqueueing Jobs

Immediate Enqueue

# Enqueue to run as soon as possible
ProcessOrderJob.perform_later(order)

# Enqueue multiple jobs at once
ActiveJob.perform_all_later(
  ProcessOrderJob.new(order1),
  ProcessOrderJob.new(order2)
)

Scheduled Execution

# Run at specific time
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order)

# Run after delay
ProcessOrderJob.set(wait: 1.hour).perform_later(order)
ProcessOrderJob.set(wait: 5.minutes).perform_later(order)

# Run immediately (blocking, no queue)
ProcessOrderJob.perform_now(order)

Queue Selection

class ProcessOrderJob < ApplicationJob
  queue_as :high_priority
end

# Or dynamically
ProcessOrderJob.set(queue: :urgent).perform_later(order)

Priority

class LowPriorityJob < ApplicationJob
  queue_as :default
  queue_with_priority 50  # Higher number = lower priority
end

# Dynamic priority
ImportantJob.set(priority: 10).perform_later(data)

Queue Adapters

Configuration

# config/application.rb or config/environments/*.rb
config.active_job.queue_adapter = :async

Common Adapters

Adapter Best For Notes
:async Development In-process, not for production
:inline Testing Runs immediately
:test Testing Stores jobs for assertions

Retries and Error Handling

Automatic Retries

class ExternalApiJob < ApplicationJob
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
  retry_on CustomTransientError, wait: 5.seconds, attempts: 3

  def perform(record)
    ExternalApi.sync(record)
  end
end

Retry Options

Option Description
wait Seconds to wait (:polynomially_longer for backoff)
attempts Number of retry attempts
queue Queue for retried job
priority Priority for retried job

Discard on Error

class DataImportJob < ApplicationJob
  discard_on ActiveJob::DeserializationError
  discard_on CustomPermanentError

  def perform(record_id)
    record = Record.find(record_id)
    # ...
  end
end

Custom Error Handling

class ProcessOrderJob < ApplicationJob
  rescue_from(PaymentError) do |exception|
    order = arguments.first
    order.mark_payment_failed!(exception.message)
    OrderMailer.payment_failed(order).deliver_later
  end

  def perform(order)
    PaymentService.charge(order)
  end
end

Callbacks

class ImportJob < ApplicationJob
  before_enqueue :log_enqueue
  after_enqueue :notify_enqueued

  before_perform :setup
  around_perform :measure_time
  after_perform :cleanup

  after_discard :log_discard

  private

  def log_enqueue
    Rails.logger.info "Enqueueing import job"
  end

  def setup
    @start_time = Time.current
  end

  def measure_time
    start = Time.current
    yield
    duration = Time.current - start
    Rails.logger.info "Job completed in #{duration}s"
  end

  def cleanup
    # Clean up resources
  end

  def log_discard(job, exception)
    Rails.logger.error "Job discarded: #{exception.message}"
  end
end

Job Arguments

Supported Types

Active Job automatically serializes:

  • Basic types (String, Integer, Float, NilClass, TrueClass, FalseClass, BigDecimal)
  • Symbol (as String)
  • Date, Time, DateTime
  • ActiveSupport::TimeWithZone
  • ActiveSupport::Duration
  • Hash (keys as strings)
  • ActiveSupport::HashWithIndifferentAccess
  • Array
  • Range
  • Module/Class
  • ActiveRecord objects (via GlobalID)

GlobalID Serialization

# ActiveRecord models are serialized via GlobalID
class NotifyUserJob < ApplicationJob
  def perform(user)
    # user is automatically deserialized from GlobalID
    UserNotifier.send_notification(user)
  end
end

NotifyUserJob.perform_later(User.find(1))
# Serialized as: "gid://myapp/User/1"

Custom Serializers

# app/serializers/money_serializer.rb
class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
  def serialize(money)
    super("amount" => money.amount, "currency" => money.currency)
  end

  def deserialize(hash)
    Money.new(hash["amount"], hash["currency"])
  end

  private

  def klass
    Money
  end
end

# config/initializers/active_job.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer

Mailer Integration

# Async email delivery
UserMailer.welcome(user).deliver_later
UserMailer.welcome(user).deliver_later(wait: 1.hour)
UserMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)

# Sync delivery (use in jobs)
UserMailer.welcome(user).deliver_now

Testing Jobs

Test Helper Methods

# test/jobs/process_order_job_test.rb
require "test_helper"

class ProcessOrderJobTest < ActiveJob::TestCase
  test "processes the order" do
    order = orders(:pending)

    ProcessOrderJob.perform_now(order)

    assert order.reload.processed?
  end

  test "enqueues job" do
    order = orders(:pending)

    assert_enqueued_with(job: ProcessOrderJob, args: [order]) do
      ProcessOrderJob.perform_later(order)
    end
  end

  test "sends confirmation email" do
    order = orders(:pending)

    assert_enqueued_emails 1 do
      ProcessOrderJob.perform_now(order)
    end
  end
end

Best Practices

  1. Keep jobs idempotent - Safe to run multiple times
  2. Use small arguments - Pass IDs, not large objects
  3. Handle missing records - Records may be deleted before job runs
  4. Set appropriate queues - Separate critical from low-priority work
  5. Monitor failed jobs - Use adapter's UI or custom monitoring
  6. Test with perform_now - Test job logic synchronously
Weekly Installs
1
GitHub Stars
2
First Seen
Apr 16, 2026