/email-for-startups providers ↗
guide

Idempotency Keys for Email APIs

Email sends are side effects. A timeout, queue replay, or user retry can turn one logical send into two delivered messages unless the request carries a stable key. This guide explains how to design idempotency keys for transactional email APIs, where provider support matters, and what fallback dedupe needs to cover.

last updated 2026-05-07 4 sections
section 01

How idempotency keys work

An idempotency key identifies one logical send attempt. The sender includes the same key every time it retries that send. The provider stores the first result for a limited window, then returns the stored result or rejects the duplicate instead of sending another message.

partjobfailure it prevents
Stable keyNames the logical send, not the HTTP attempt.Duplicate email after timeout.
Provider cacheStores the accepted request and response for a retention window.Duplicate send after retry.
Client retryReuses the original key until the send succeeds or is abandoned.New key per retry.
Local ledgerRecords the key and message state in the app database.Provider cache expiry surprises.
section 02

Generating keys that survive retries

A useful key is deterministic for the action that caused the send. Password reset, magic link, receipt, and invoice emails should derive the key from the business event ID plus the template or stream name. Random UUIDs are fine only when they are generated before the first attempt and stored before the request leaves the app.

  • ok Derive the key from a durable event ID when one exists.
  • ok Store the key before calling the provider.
  • ok Reuse the same key for every retry of the same logical send.
  • ok Scope keys by message stream when the same event sends multiple emails.
  • ok Do not include mutable template copy or recipient display names in the key.
section 03

Retry windows and provider gaps

Provider idempotency windows are not a substitute for application state. A queue can replay a job after the provider cache expires, a worker can crash between API success and database update, and some providers expose no first-class idempotency key at all. Treat provider idempotency as one layer, then keep a local send ledger for final control.

caseriskcontrol
Provider supports keysCache expires before late replay.Keep local send state keyed by event ID.
Provider lacks keysEvery HTTP retry can send again.Lock locally before each provider call.
Worker crash after sendApp does not record provider success.Store pending state first, then reconcile by message ID.
Template fan-outOne event sends several messages.Include template or stream in the key.
section 04

Testing duplicate-send protection

Idempotency tests should force the failure modes that real systems hit: client timeout, worker retry, queue replay, and webhook delay. The passing result is boring: one provider message ID, one send ledger row, and no second delivery attempt.

  • ok Send once, retry the exact request, confirm only one provider message ID.
  • ok Crash the worker after provider success, then replay the job.
  • ok Replay the queue job after the normal retry window.
  • ok Send two different templates from the same event and confirm both are allowed.

related startup email pages