← Blog

Engineering · · 4 min read

How do I stop Stripe retries from doing the work twice?

Understand duplicate Stripe webhooks, event ordering, idempotency keys, concurrent workers, and unknown outcomes before retrying agent actions.

Deixic

A payment event arrives twice, a worker restarts, or an agent retries an unfinished task. The customer should not receive another fulfillment, email, or attempted change because the machinery repeated itself.

A developer's webhook design question describes duplicate-key errors despite checking for an existing event. Another asks how to handle exceptions and retries. Together they show two jobs: accepting events once and safely repeating an outbound request.

Can Stripe deliver the same webhook more than once?#

Yes. Stripe's webhook guide documents duplicate delivery and recommends recording processed event IDs. Separately generated events can concern the same object, so the business action needs its own identity as well.

Model those identities separately. The event says what notification arrived. The business operation says what you intend to do because of it.

"Grant the entitlement for this completed purchase" should not become three operations because three paths noticed the purchase. Derive the business key from the purchase and the entitlement, and leave the transport attempt out.

Why does checking the event ID still produce duplicates?#

Two workers can both check before either saves, and the unique-constraint failure in the public design question above reveals this race. A database constraint or equivalent atomic claim has to decide ownership, since a preceding read cannot guarantee it.

In your implementation:

  • Accept the event and create its durable work item in one transaction.
  • If a separate queue is involved, use an outbox so the pending enqueue survives a crash.
  • Have workers claim work atomically and recover abandoned claims under an explicit policy.

Do not catch a duplicate-key exception and then continue with the side effect. Distinguish "event received" from "operation completed": recording receipt before work does not mean the work succeeded.

Are webhook events guaranteed to arrive in order?#

No. Stripe's webhook guide says event ordering is not guaranteed. Decide the latest business state from the relevant object and operation, and do not assume the last event received is the newest.

In an entitlement workflow, an older event should not overwrite a more recent cancellation because it arrived later. Persist the event history, reconcile against authoritative object state when needed, and make state transitions explicit, such as "older observation retained; current entitlement unchanged."

Test an out-of-order pair, duplicate delivery, and a reconciliation job racing the webhook. The invariant is the correct final business state.

Should a retry use the same idempotency key?#

For a retry of the same Stripe operation with the same parameters, keep the key. Stripe's idempotent request reference describes returning the saved result for that key and rejecting mismatched parameters. Keys are pruned after their retention period and do not serve as a permanent business ledger.

Create and persist the key before dispatch. An agent that generates a fresh key on each timeout no longer identifies one operation.

The public parameter-mismatch question shows that changing the request is a new decision. Resolve what happened to the original operation first, then decide whether a distinct operation is authorized.

What should happen after a timeout or a saved server error?#

Treat the outcome as uncertain until you can establish it. The idempotency reference states that a saved result can include an error response. Do not rotate keys to escape it, since that loses the relationship to the original attempt.

Reconcile using the operation's provider identifiers, request references, and later events. Escalate an unresolved write with the exact target and attempt history.

Application observation Safe interpretation
Request never dispatched The operation has not been attempted by this worker
Provider confirms success Save the object and result against the operation
Connection lost after dispatch Outcome unknown; investigate before a new operation
Same webhook received again Existing event, possibly existing unfinished work
Parameters now differ Reassess the intended change and its authorization

Deixic's Stripe connector lists its reads and writes. For account research spanning Salesforce and Stripe, keep the customer mapping too. A perfectly deduplicated action on the wrong customer is still the wrong action.

Find another symptom in the agent troubleshooting question index.