Introduction
Background jobs can fail due to network timeouts, rate limits, or missing records. Active Job provides built-in retry and discard mechanisms to handle failures gracefully without losing work.
Key Concepts
- retry_on: Automatically retries a job when a specific exception occurs, with configurable wait time and attempt limit.
- discard_on: Silently discards a job when a specific exception occurs — useful for unrecoverable errors.
- Idempotency: Designing jobs so running them multiple times produces the same result as running once.
Real World Context
A payment processing job that retries on network timeout but discards on invalid card errors ensures payments eventually succeed without charging a declined card repeatedly.
Deep Dive
Automatic Retries
rubyclass ImportDataJob < ApplicationJob retry_on StandardError, wait: :polynomially_longer, attempts: 3 retry_on Timeout::Error, wait: 5.seconds, attempts: 5 discard_on ActiveJob::DeserializationError discard_on ActiveRecord::RecordNotFound def perform(import_id) import = Import.find(import_id) import.process! end end
Custom Retry Logic
rubyclass ApiCallJob < ApplicationJob retry_on ApiRateLimitError, wait: ->(executions) { (executions ** 2).seconds # 1s, 4s, 16s, 64s... }, attempts: 5 def perform(endpoint) ApiClient.call(endpoint) end end
Error Callbacks
rubyclass CriticalJob < ApplicationJob rescue_from StandardError do |exception| ErrorTracker.report(exception) raise exception # Re-raise to trigger retry end after_discard do |job, exception| AdminMailer.job_failed(job, exception).deliver_later end def perform(data) # ... end end
Idempotency Pattern
rubyclass ChargeOrderJob < ApplicationJob def perform(order_id) order = Order.find(order_id) return if order.charged? order.with_lock do return if order.charged? charge = PaymentGateway.charge(order.total) order.update!(charged: true, charge_id: charge.id) end end end
Common Pitfalls
- Not designing for idempotency — Jobs may run multiple times due to retries or queue failures. Always check if the work was already done before proceeding.
- Retrying on all errors — Some errors (invalid arguments, missing records) will never succeed. Use
discard_onfor unrecoverable errors.
Best Practices
- Use polynomially_longer wait — Exponential backoff avoids overwhelming a failing service with rapid retries.
- Lock records before mutating — Use
with_lockto prevent race conditions when multiple job executions overlap.
Summary
- Use
retry_onwith exponential backoff for transient failures. - Use
discard_onfor errors that will never succeed on retry. - Design every job to be idempotent — safe to run multiple times.
- Use
with_lockto prevent race conditions in critical operations.
Code Examples
ruby
class PaymentJob < ApplicationJob
retry_on Timeout::Error, wait: :polynomially_longer, attempts: 3
discard_on ActiveRecord::RecordNotFound
def perform(order_id)
order = Order.find(order_id)
return if order.charged? # Idempotency guard
order.with_lock do
return if order.charged?
charge = PaymentGateway.charge(order.total)
order.update!(charged: true, charge_id: charge.id)
end
end
end