How we implemented idempotency keys in our email API.
We just released idempotency keys for our email API.
While adding idempotency to applications is a common pattern, the process includes a lot of details. We learned valuable lessons we'd like to share the process.
An idempotent operation is an action you can perform more than once, with the same input, and it always produces the same outcome and avoids repeating side effects.
Once an operation is idempotent, you can safely retry it without risking causing its effects more than once (e.g., sending an email again, using up quota twice, etc.).
We added support for idempotency keys on the POST /emails
endpoint.
By adding an Idempotency-Key
header, or using the equivalent field on our SDKs, you can tell Resend that this specific email should only be sent once, even if we get more than one request from you about it.
await resend.emails.send({from: 'Acme <onboarding@resend.dev>',to: ['delivered@resend.dev'],subject: 'hello world',html: '<p>it works!</p>',},{idempotencyKey: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',},);
We mark the request as idempotent by checking the Idempotency-Key
header and the request payload.
Sending the same payload when you retry a request with an idempotency key is important. We check the request payload to avoid unexpected side effects, such as suppressing unrelated emails. For instance, if you accidentally set the same string for all your idempotency keys, on different emails, and we didn't check that, only your first email would be delivered. By checking the payload, we can detect this is a mistake and let you know in the API response.
Idempotency is a key ingredient for building reliable systems.
Here are a few key benefits for our customers:
The exact format of your idempotency keys is up to you, but here are some recommendations.
Idempotency keys can be formatted to include an event type (e.g., order-sent
) and an entity ID (e.g., 12345678
).
Here are some common scenarios:
Order tracking: combine an order ID with an event type
Single-events: for events that happen only once per user, combine the user ID with an event type
Daily notifications: consider including the date in the key
Note that for us there is no special meaning to the separator characters or event names. As long as the keys are 1–256 characters, Resend will hash and store them. The specific format you use is up to you.
Importantly, for idempotency to work, the key must identify exactly one email you want to send. Avoid the following patterns:
Hard-coded keys only
Random UUIDs for the same email
When we receive an email to send with a valid idempotency key, we first check against existing emails with the same key. The process leads either to an error, the email being sent for the first time, or the cached response being returned.
Here is a full diagram of how we've implemented idempotency keys:
To thoroughly test our implementation, we pursued three main strategies.
1. Automated tests
We wrote automated tests to validate the idempotency key implementation.
2. Internal usage
We added idempotency key to our own usage of Resend's API (e.g., team invitations and quota threshold notifications) and then introduced deliberate retries in some of these flows to validate that emails were not sent more than once.
3. User testing
We reached out to users who previously requested this feature to try it out, validate and give us feedback.
Once confident in our internal and user testing, we rolled out the feature to all users. Because using the feature requires sending a specific header, it doesn't impact existing workflows.
During the implementation, we made several decisions about our implementation.
Since many of our users use our SDKs, we needed to model idempotency in natural ways for each of the platforms we support.
We decided to implement it on an API request level, instead of the data model object. This allows us to in the future generalise the implementation to other endpoints more easily, and also support endpoints that don't relate to a single entity, like the batch email endpoint.
We decided to normalize and hash the request body and store it in the idempotency cache with its response. This allows us to check for accidental idempotency key reuse without storing large amounts of data unnecessarily.
We referred to community resources and other tech companies implementations of idempotency and to requests from our users who were interested in this feature. Some useful resources include:
Support for idempotency keys was a highly requested feature, and we're happy to bring it to our users. We anticipate many useful use cases for it, and we're sure you will find even more.
We're excited to include this important feature in our API and hope this post helps you implement it in your own applications. If you have any questions, reach out to us and we'll be happy to help.