FR | EN
Parler de votre projet

Idempotent by Design: Making Your Sidekiq Jobs Safe to Retry

Sidekiq guarantees at-least-once delivery, not exactly-once. Any job that is not safe to run twice is a bug waiting for the right overlap.

Publié le 5 février 2023 · 4 min de lecture

Sidekiq's contract is at-least-once delivery, not exactly-once. A job can run twice: a worker times out after finishing but before acknowledging, a deploy restarts a process mid-job, Redis fails over. Any job that isn't safe to run twice is a bug waiting for that overlap to happen.

The classic failure: double charging

# Not idempotent: runs twice, charges twice
class ChargeCardJob
  include Sidekiq::Job
  def perform(order_id)
    order = Order.find(order_id)
    PaymentGateway.charge(order.amount_cents, order.card_token)
    order.update!(charged: true)
  end
end

If the process dies after the gateway call succeeds but before the update commits, Sidekiq retries the job, and the gateway gets called a second time with no way to know it already ran.

An idempotency key closes the gap

Most payment gateways, and plenty of other external APIs, accept an idempotency key: pass the same key twice and the second call returns the first call's result instead of executing again. Generate that key from something stable about the job, not from a random value, or a retry will generate a new key and defeat the whole point.

class ChargeCardJob
  include Sidekiq::Job
  def perform(order_id)
    order = Order.find(order_id)
    return if order.charged?

    PaymentGateway.charge(
      order.amount_cents,
      order.card_token,
      idempotency_key: "charge-order-#{order.id}"
    )
    order.update!(charged: true)
  end
end

Guard the check-then-act gap too

The early return on an already-charged order helps, but two overlapping runs of the same job can both pass that check before either one updates the record. A unique database constraint, or wrapping the read and the flag update in a locked transaction, closes that remaining window instead of relying on timing alone.

Idempotency isn't only for payments

Sending an email twice is a minor embarrassment; charging a card twice is a support ticket and a chargeback. But the same discipline, an operation-specific key, a check against prior execution, applies to any job with a side effect outside your own database: emails, webhooks fired to a partner, third-party API calls.