Yeda AI Tips · #086

Español

Test Background Jobs for Idempotency and Retries

Your background job will run twice. If you did not test for that, someone gets double charged. That's not pessimism — it's the delivery contract. Sidekiq's own docs say it plainly: "Sidekiq will execute your job at least once, not exactly once." Amazon SQS standard queues make the same promise: at-least-once delivery, and "more than one copy of a message might be delivered." Duplicates aren't a bug in your queue. They're the spec.

Why every queue re-runs your job

Job systems can only guarantee a job wasn't lost by re-sending it whenever they're unsure it finished. The unsure cases are everywhere:

Run twice means charge twice, email twice, ship twice — unless the second run is harmless. That property has a name: idempotency. Celery's task guide defines the goal exactly: "the function won't cause unintended effects even if called multiple times with the same arguments."

The two tests every job needs

TestWhat you assertCatches
IdempotencyRun the job twice with the same args; the world looks identical to running it once — one charge, one email, one rowDuplicate delivery, double-click enqueues, ack-after-work crashes
Retry recoveryFail the job mid-way (raise after the first side effect), then re-run it; it completes without corrupting stateHalf-done work, retry storms, poison-pill jobs

In code, the idempotency test is almost boring — which is the point:

def test_charge_job_is_idempotent(db, payment_gateway):
    charge_order(order_id=42)
    charge_order(order_id=42)          # duplicate delivery

    assert payment_gateway.charge_count(order_id=42) == 1
    assert db.order(42).status == "paid"

The retry test injects the failure the queue will eventually inject for you:

def test_charge_job_recovers_after_crash(db, payment_gateway):
    with payment_gateway.fail_after_charge():   # crash between work and ack
        with pytest.raises(GatewayTimeout):
            charge_order(order_id=42)

    charge_order(order_id=42)                   # the retry
    assert payment_gateway.charge_count(order_id=42) == 1

If either test can't pass, the fix is usually one of three patterns: a unique key on the side effect (e.g. an idempotency key sent to the payment provider), a state check before acting ("skip if already paid"), or making the whole job transactional so a crash rolls back to a clean re-runnable state — Sidekiq's best-practices page calls this pair out directly: make jobs idempotent and transactional.

Power moves

Now a duplicate delivery is safe behavior, not a support ticket.

Resources

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog