ruby 90 lines · 3 tabs

Exponential Backoff with Jitter for Flaky External API Calls in ActiveJob

Shared by codesnips Jul 2026
3 tabs
class SyncContactJob < ApplicationJob
  queue_as :external

  BACKOFF = ->(executions) do
    (2**executions) + rand(0.0..1.0) # exponential + jitter, in seconds
  end

  retry_on TransientError, wait: BACKOFF, attempts: 6

  discard_on PermanentError do |_job, error|
    Rails.logger.warn("Dropping contact sync: #{error.message}")
  end

  def perform(contact_id)
    contact = Contact.find(contact_id)
    client = ContactApiClient.new

    result = client.upsert(contact)
    contact.update!(
      external_id: result.fetch("id"),
      synced_at: Time.current
    )
  end
end
3 files · ruby Explain with highlit

This snippet shows how a flaky external HTTP call is made resilient inside a Rails ActiveJob, without hand-rolling loops or letting transient failures silently drop work. The core idea is that network calls fail for boring, temporary reasons — timeouts, 429 Too Many Requests, 5xx blips — and the correct response is not to fail permanently but to retry a bounded number of times with growing gaps between attempts.

In SyncContactJob, the job leans on ActiveJob's built-in retry_on rather than a manual begin/rescue/sleep loop, because retries handled by the queue adapter release the worker thread between attempts instead of blocking it. Only TransientError triggers retries; a PermanentError (like a 4xx that will never succeed) calls discard_on so the job is dropped instead of wasting the queue. The wait: option is passed a lambda so the delay is computed per-attempt: 2 ** executions gives exponential growth, and a random rand term adds jitter. Jitter matters because when a downstream service recovers, many jobs that failed at once would otherwise retry in lockstep and hammer it again — the classic thundering-herd problem.

The perform method delegates the actual call to ContactApiClient, keeping the job focused on orchestration and error policy. Because retries mean perform can run more than once, the request in ContactApiClient sends an Idempotency-Key derived from the contact so the remote side can dedupe repeated deliveries — essential when a request succeeds server-side but the response is lost to a timeout.

In ContactApiClient, HTTP status codes are translated into the two domain exceptions the job cares about: raise_for_status maps 5xx and 429 to TransientError and other non-2xx codes to PermanentError. Low-level Faraday::TimeoutError and connection failures are also rescued and reclassified as transient. This separation means the retry policy lives in one place and the client stays a thin, testable wrapper. The trade-off is that retries add latency and require the downstream endpoint to tolerate duplicates; the attempts cap and discard_on guard against retrying forever. A developer reaches for this pattern whenever a job crosses a network boundary to a service they do not control.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Exponential Backoff with Jitter for Flaky External API Calls in ActiveJob — share card
Link copied