Retry with backoff
Defer a failed operation to a durable retry with exponential backoff. Worked examples for Stripe (idempotency-key handling for timeouts and custom dunning); same shape applies to CRM syncs, webhook deliveries, and any external call that occasionally fails.
Use this to
- Retry a Stripe payment that timed out using the same idempotency key (Stripe deduplicates against the original)
- Run a custom invoice dunning sequence beyond Stripe Smart Retries with a new key per attempt
- Retry a CRM sync that timed out during signup
- Retry a 500-failing webhook delivery with backoff over the next day
Code
Stripe timeout: reuse the idempotency key on retry
// the call timed out, so you don't know whether Stripe processed it. // retry with the SAME idempotency key. Stripe returns the cached // result if the original succeeded, so the customer is never charged // twice. don't auto-retry card_declined — the bank already said no // and the cached error will come back unchanged. try { await stripe.paymentIntents.create( { amount, customer, confirm: true }, { idempotencyKey: `charge-${order.id}` }, ); } catch (err) { if (isTransientError(err)) { await dk.schedule("retry-charge", { key: order.id, delay: "30s" }); } } dk.handle("retry-charge", { handler: async ({ key }) => { const order = await db.orders.find(key); if (order.status === "paid") return; // resolved by another path await stripe.paymentIntents.create( { amount: order.amount, customer: order.customerId, confirm: true }, { idempotencyKey: `charge-${key}` }, // SAME key as the original ); }, retry: { attempts: 3, backoff: "exponential" }, });
Stripe invoice dunning: a new idempotency key per attempt
// after Smart Retries exhaust (or if you've turned it off), // run your own dunning schedule. each attempt is a NEW payment, // so each gets its own idempotency key. reusing the key would // dedupe back to the first failure and never actually retry. dk.handle("retry-invoice", async ({ key }) => { const [invoiceId, attempt] = key.split(":"); const invoice = await stripe.invoices.retrieve(invoiceId); if (invoice.status !== "open") return; // paid or voided await stripe.invoices.pay(invoiceId, undefined, { idempotencyKey: `dunning-${invoiceId}-${attempt}`, }); });
npm install delaykit
bun add delaykit