Introduction
Jobs fail — networks drop, APIs rate-limit, databases lock. Active Job provides retry_on and discard_on to handle failures gracefully with configurable backoff strategies.
Key Concepts
- retry_on: Rescues a specific exception and re-enqueues after a wait period.
- discard_on: Rescues an exception and silently discards the job.
- Exponential Backoff: Retry wait times increase with each attempt.
Real World Context
A payment API job might fail from a temporary network issue — retrying in 30 seconds will likely succeed. But a job failing due to a deleted user should not retry.
Deep Dive
rubyclass ApiSyncJob < ApplicationJob retry_on StandardError, wait: :polynomially_longer, attempts: 3 retry_on Timeout::Error, wait: 5.seconds, attempts: 5 retry_on RateLimitError, wait: 1.minute, attempts: 10 discard_on ActiveRecord::RecordNotFound discard_on ArgumentError def perform(record_id) record = Record.find(record_id) ExternalApi.sync(record) end end
Custom Backoff with Jitter
rubyclass ResilientJob < ApplicationJob retry_on ApiError, wait: ->(executions) { (executions ** 2) + rand(30) }, attempts: 5 end
Common Pitfalls
- Retrying non-transient errors — Retrying invalid input wastes resources. Use
discard_on. - Not adding jitter — Simultaneous retries create storms. Add randomness.
Best Practices
- Be specific with error classes — Don't rescue
StandardErrorwhen you meanTimeout::Error. - Set appropriate attempt limits — 5 attempts covers most transient failures.
Summary
- Use
retry_onfor transient errors,discard_onfor permanent ones. wait: :polynomially_longerprovides increasing backoff.- Add jitter to prevent retry storms.
- Be specific about which exceptions to handle.
Code Examples
ruby
class PaymentSyncJob < ApplicationJob
retry_on PaymentGateway::TimeoutError, wait: :polynomially_longer, attempts: 5
retry_on PaymentGateway::RateLimitError, wait: 1.minute, attempts: 10
discard_on PaymentGateway::InvalidCardError
def perform(payment_id)
payment = Payment.find(payment_id)
PaymentGateway.sync(payment)
end
end