This is a hypothetical example, drafted with AI assistance.
A refund worker sends an approved $20 refund against a $100 payment. The provider creates the refund, but the response never reaches the worker.
Then an outage delays recovery.
Forty-eight hours later, the job runs again. It has the original idempotency key and the same request parameters.
Is it safe to send?
For Stripe API v1, keys may be pruned once they are at least 24 hours old. Reusing a key after pruning generates a new request. Forty-eight hours does not prove the key is gone, but the worker cannot rely on it still being retained.
The provider may still have the refund while no longer retaining the key that protected its creation request.
That is the failure this article explores: how to stop an old job from automatically creating another refund while the first outcome remains unresolved.
Source: https://docs.stripe.com/api/idempotent_requests
Start with the approved operation
The customer was approved for one $20 refund. A timeout did not approve another.
Before dispatching the first request, persist a business-operation record containing the approval, amount, currency, payment reference, provider account, original idempotency key and first-attempt time. Record subsequent attempts and any provider refund ID. Retain enough history to recognize the operation for as long as old jobs can be replayed.
Every replay must load that same operation.
The important part is what the worker does with it. If a replay simply reads the saved key and sends again, the record has preserved history without preventing the duplicate.
The record must also govern whether another request is allowed.
Make the recovery decision explicit
Define a retry deadline conservatively within the provider’s supported retention window. Keep the original deadline across restarts, queue redelivery and manual replay.
For our proposed recovery policy:
- If the original refund is already identified, follow that refund’s status rather than creating another.
- If the outcome is unknown and a same-key retry is still eligible under the provider’s contract and application policy, the worker may retry with the original parameters.
- If the outcome is unknown and the deadline has passed, move the operation to reconciliation. Do not authorize another creation request from that replay.
The support team’s retry button must follow the same decision.
At hour 48, our job takes the third path. Its next task is to investigate the original refund, rather than send the request again.
This rule blocks ordinary expired-job replay. It has an important limit: a worker could pass its eligibility check, pause, and send later. A local deadline or database lock cannot revoke a request already in flight.
That outstanding-request risk needs separate analysis. The policy here must not be presented as a universal exactly-once guarantee.
Find out what happened to the first refund
If the operation already has a provider refund ID, retrieve that refund.
Otherwise, inspect refunds for the correct provider account and payment. Stripe supports filtering by charge or PaymentIntent and paginating the results.
An opaque business-operation ID attached as metadata can help establish the match. Amount alone is insufficient: the same payment could have two separately approved $20 refunds.
Suppose reconciliation finds the refund from our first attempt. Link its ID to the original operation and inspect its status.
A pending refund still needs tracking. A succeeded refund gives a different outcome. A failed or canceled refund needs a separate recovery decision. Finding an ID alone does not establish successful completion.
In our example, identifying the original refund lets the system continue tracking it without creating another.
Sources:
https://docs.stripe.com/api/refunds/list
https://docs.stripe.com/api/refunds/object
What if reconciliation finds nothing?
Keep the operation unresolved.
An empty lookup does not, by itself, establish that the first request never executed or cannot execute later. Neither does a missing local response.
Record what was checked, assign an owner and set a next review time. Give Support an accurate status rather than turning uncertainty into “failed.”
This article does not provide a generic rule for safely reissuing after an inconclusive lookup. That requires evidence specific to the provider and execution path, including outstanding attempts. A manual decision does not remove that requirement.
Until those conditions are established, automatic reissue stays blocked.
Test the decision we just described
Use a provider stub with separate storage for refund objects and idempotency entries, plus a controllable clock.
Test the original scenario:
- Create the $20 refund, then drop the response.
- Restart the worker from persisted state.
- Advance 48 hours and prune the stub’s idempotency entry, leaving the refund intact.
- Replay through both the queue and the support recovery path.
- Verify that neither replay sends another refund-creation request.
Then test reconciliation: expose the original refund and verify that its ID and actual status are recorded. With inconclusive evidence, verify that the operation remains unresolved without reissuance.
As a negative control, bypass the replay restriction and resend the original key after pruning. With enough unrefunded money remaining on the $100 payment, the stub should create a second $20 refund. The duplicate-detection assertion should fail.
Separately pause a worker after its eligibility check and resume it after the deadline. This tests the stated limitation; a late request escaping means the cutoff alone does not establish end-to-end safety.
These are proposed tests, not executed results. They check this recovery policy under the stub’s assumptions, not every possible provider or network behavior.
The release question
For an old refund job, reusing a key is only part of the story. The system also needs to preserve the original approval, stop unsafe automatic replay and make unresolved outcomes visible.
I’m building Ocean around evidence-backed review of financial software changes. For this flow, the useful question is specific:
When this job returns after the provider’s key-retention window, what stops the replay from turning one approved refund into two?
A convincing answer needs both the evidence and its limits.
