DEV Community

Swipe Again for Swipe Again

Posted on

Re-Read the Invoice Before Every Retry: The Boring Rule That Prevents Double Charges

Every retry system eventually faces the same fear: what if the customer already paid? A retry fired against an invoice that was settled an hour ago — through a card update, a manual payment, or the billing platform's own native retry — is not recovery. It is a double charge, a chargeback risk, and a trust-destroying email thread.

The fix is unglamorous: never trust your own cache of the invoice state at the moment you fire. In Swipe Again's retry engine, every attempt re-reads the invoice's live state from the platform immediately before executing, claims the attempt row with a compare-and-swap update so two workers can never fire the same attempt, and carries an idempotency key so a retried API call cannot create a second charge. Recovery itself is recorded through a single code path, which makes double-counting structurally impossible rather than merely unlikely. The details of how this interacts with Stripe's native schedule are in our Stripe recovery guide.

This matters more when you deliberately retry around a native schedule instead of replacing it. Platform retries and third-party retries running in parallel is exactly the configuration where stale state hurts you: the window between "their attempt succeeded" and "your system noticed" is where double charges live. Closing that window is a read, not a rewrite — one fresh fetch of the invoice, every time, no exceptions for performance.

It costs a few hundred milliseconds per attempt. It has never once been the expensive option.

The broader recovery flow is built on the same principle: state lives on the platform, and the recovery layer's job is to read it honestly, act once, and record what happened in exactly one place.

Top comments (0)