Skip to main content

Command Palette

Search for a command to run...

After the idempotency window: a late retry decision

Updated
•5 min read•View as Markdown

A queued retry can still carry the original key after the provider has stopped remembering it. The key in your database and the provider's deduplication record have different lifetimes. A recovery process needs to account for both.

Resend documents a 24-hour retention period for email idempotency keys. The documented API endpoints are POST /emails and POST /emails/batch. The documentation describes checking requests against the last 24 hours and returning the same response without sending again within that contract. This is one provider's documented window, not a permanent guarantee for every notification integration.

A hypothetical 25-hour gap

This timeline is a thought experiment, not an observed incident or a result from the Java sample. Assume a provider deduplicates identical requests for 24 hours, then removes the relevant record. The example does not establish Resend's exact deletion schedule.

  • Monday, 08:00: A client sends the synthetic operation demo-notification-001. The provider accepts it, but the response is lost. The client records an uncertain result.

  • Monday, 08:02: A recovery job is delayed. The application keeps the same operation key and payload.

  • Tuesday, 09:00: The job resumes, 25 hours after the original request. Under the example's assumptions, the provider no longer has the original deduplication record.

  • If the client now sends again: Even the unchanged key and payload can produce another acceptance. The earlier effect did not disappear when the deduplication record expired.

The useful review question is whether the delayed job still falls inside the provider's documented protection. Preserving a key across recovery is necessary for a stable-key strategy, but it does not extend the provider's retention window.

If the original request time or the retention boundary is uncertain, that uncertainty belongs in the decision too. A local timestamp should not be presented as proof of exactly when a provider began or ended its guarantee.

An expired window does not settle the original result

After the hypothetical gap, the application has two separate questions: was the first operation accepted, and would another attempt be deduplicated? Losing protection for the second question does not answer the first.

A lookup can help only when its contract and correlation are known:

  • A trustworthy positive acceptance record: If it matches the same provider account, operation and intended content, record the operation as accepted and avoid sending it again. Acceptance still does not prove recipient delivery.

  • NOT_FOUND: Check what absence means for that lookup's scope, visibility and record lifetime. Unless the provider explicitly promises definitive non-acceptance for this situation, a missing record does not establish that the original operation failed. It is not automatic permission to resend.

  • No supported lookup, or a lookup that times out: Preserve the unresolved state and the observation. Inventing a new key does not recover evidence about the first attempt.

These are review rules, not a claim that Resend exposes a lookup API by your client-supplied key. Confirm the actual lookup interface before designing around it. Where the result remains uncertain, a next action needs an explicit business decision about duplicate risk, stronger evidence, or a documented provider guarantee.

A small contract checklist for delayed work

Before allowing an old job back into an automatic send path, write down:

  • The endpoint, account and operation covered by deduplication, including same-key/different-payload behavior.

  • The retention period and how it is defined. Identify which part of the original attempt's timing you actually know.

  • The lookup's correlation, retention and visibility rules, and what positive, missing or unavailable results prove.

  • The oldest work the application can replay, and the decision when that age exceeds the documented protection.

Retain the original operation identity, intended-content reference, attempt observations and evidence for the eventual decision. That application record can preserve context after a provider cache expires; it does not itself prevent a second remote effect. This checklist is a design review aid, not production certification.

What the free sample covers

Download the free MIT Java 21 sample or inspect its source and explicit fake-provider contract.

The sample covers in-memory timeout ambiguity, stable-key retries, stopping without provider deduplication, and a read-only acceptance lookup. A positive fake lookup resolves UNKNOWN_RECONCILE_REQUIRED to ACCEPTED; a missing result leaves it uncertain. Its seven executable checks exercise that teaching contract.

The sample does not simulate key expiry. The fake retains records for the lifetime of one object; a fresh object starts empty. It does not test persistent recovery, real provider lookup, real network failures or production readiness. The 25-hour timeline above cannot be reproduced as an expiry test by running this sample.

Run ./run.sh with JDK 21 and a POSIX shell. The README also gives direct javac/java commands for Windows. No Docker, database, dependency manager, provider credentials or network calls are required. You can use the sample without buying anything.

Disclosure: This article and the sample were created with AI assistance and reviewed against the stated source contract and official documentation. Adrian Studio sells the related Notification Failure Lab for Spring Boot, USD 29 once for one named purchaser under its separate buyer license. The paid educational lab adds PostgreSQL-backed persisted-work recovery, competing-worker exercises and duplicate/out-of-order receipt transitions; this article makes no new claim that it tests provider-key expiry.