All lessons

Spot the Tax · Card 13 of 20

Background jobs must be safe to run twice

Why a Sidekiq retry will charge your customer a second time if the job isn't idempotent.

The code

What will this cost you in six months?

class ChargeUserJob < ApplicationJob
  def perform(user_id, amount_cents)
    user = User.find(user_id)
    Stripe::Charge.create(
      amount: amount_cents,
      currency: "usd",
      customer: user.stripe_customer_id
    )
  end
end

The problem

Sidekiq automatically retries jobs that fail. The failure could be a network blip mid-API-call, a worker getting a SIGTERM during a deploy, a timeout on the response from Stripe. In all of those cases the API call may have already succeeded — you just didn't get the response back. Sidekiq then runs the job again and Stripe charges the customer a second time. The job worked. It just worked twice. The customer calls support.

Take a moment. Before revealing, think about what would have to be true for the job to be safe to run any number of times. What state do you need to track, and where?