Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To prevent duplicate emails when an API request times out and retries, give each logical send one stable idempotency key, enforce that key with a PostgreSQL unique constraint, and record the send intent in an outbox transaction. Then have a worker submit it using the same key with the email provider if the provider supports idempotency. This makes the local claim race-safe and the work recoverable; it cannot make PostgreSQL and an external email service commit atomically or guarantee exactly-once delivery.

What an idempotency key does—and does not—guarantee

An idempotency key identifies one logical email operation across HTTP retries. If a client times out, it may not know whether the server accepted its request. Repeating the request with the same key lets the API recognize it as a retry rather than create another send operation.

There are two separate boundaries. PostgreSQL can ensure that only one local operation claims a properly scoped key. The email provider, however, performs the actual external side effect. A database transaction cannot atomically commit both the database changes and the provider’s send, so a crash at the boundary can leave the outcome uncertain. Use durable work records, provider-side deduplication where available, and reconciliation—not an exactly-once promise.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a stable key for the logical send

Generate or derive the key before the first attempt, then persist it. Every retry of that same send must use the same key; generating a new UUID inside each retry path defeats deduplication. A random key is suitable only if it is created once and saved durably before the first provider request.

Scope the key to the business operation that it represents. If one business event legitimately produces several messages, include the message stream or template identity so each send has a distinct key. Include tenant or account scope where needed to prevent unrelated operations from colliding, while ensuring retries within that scope resolve to the same record.

Store enough information to resolve a retry consistently: the scoped key, a request fingerprint or equivalent payload identity, processing state, provider or message identifier when known, and the response or result the API should return. If the same key arrives with different content, do not silently overwrite the original receipt. Reject or otherwise handle the mismatch according to the API contract.

Use a unique constraint as the concurrency gate

An application-level “look up, then insert” check is not race-safe by itself: two concurrent requests can both see no record before either inserts. Instead, enforce uniqueness in PostgreSQL with a unique constraint or unique index on the correctly scoped key. PostgreSQL’s unique-index documentation explains that a potential conflict with an uncommitted row makes an insert wait, then check again after the other transaction commits or aborts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When the competing request loses the uniqueness race, return the original stored result or the operation’s current state; do not start a second send. PostgreSQL describes unique indexes as indexes that disallow multiple entries with identical keys in its PostgreSQL 14 documentation.

Put the send intent in a transactional outbox

Write the business change and an outbox row in the same PostgreSQL transaction. A worker reads pending rows, calls the email provider, and records the result. This ensures a committed business event does not lose its send intent if the application crashes before handing work to a non-durable queue.

  1. Accept the request: validate the key and payload, then attempt to create the idempotency record under its unique constraint.
  2. Commit local intent: in the same transaction as the relevant business change, save the outbox work and the operation state. If the key already exists, resolve the request from that existing operation instead of creating another outbox item.
  3. Submit asynchronously: have a worker claim pending work and call the provider with the stable provider idempotency key when supported.
  4. Persist the outcome: record the provider response and identifier when known, and expose the result or current status to retries according to the API contract.
  5. Reconcile ambiguity: if the worker crashes after provider acceptance but before its local success update, do not assume PostgreSQL can tell whether the email was sent. Retry with provider idempotency where available, or use the provider’s lookup or event mechanisms if it exposes them.

A committed “processing” marker does not prove delivery. Persist states that let the worker distinguish pending work from submitted or ambiguous work and confirmed outcomes. The exact state transitions and retention period depend on the provider’s behavior and the API contract; there is no universal state machine established by the cited sources.

Use PostgreSQL notifications only as wake-up signals

NOTIFY can prompt a worker to check for new outbox rows, but the table—not the notification—is the durable record of pending email. PostgreSQL delivers notifications only after the notifying transaction completes, and identical channel-and-payload notifications within one transaction can be folded into one. The PostgreSQL 15 NOTIFY documentation recommends storing large amounts of information in a table and sending the record key. Treat a notification as a hint to scan durable work, not as one guaranteed message per outbox row.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Check the provider’s idempotency rules

Provider behavior is not interchangeable. Before relying on a provider key, verify its scope, retention window, handling of expired keys, behavior for different payloads, concurrent in-flight requests, cached response codes, partial batch outcomes, and whether retries replay the original response and message identifier.

Anypost’s documented behavior

Anypost’s API reference describes an optional Idempotency-Key of 1–255 printable ASCII bytes, scoped per team across its single-send and batch endpoints. For the documented behavior, a same-key, same-body request replays the original response within 24 hours; a different body returns 422 idempotency_mismatch; an in-flight duplicate returns 409 idempotency_concurrent; and 5xx responses are not cached. These are Anypost-specific rules, not general email API defaults; confirm the live reference before building against them.

Posthaste’s documented behavior

Posthaste’s idempotency guidance says replay comparison is based on the key: reusing a key for different content can return the original message result while discarding the new content. Its guidance is to assign a key to one particular message and reuse it across retries. Validate the payload locally so a client cannot accidentally reuse a key for a changed email.

Design the retry response around stored state

The API should have a defined answer for each retry case. A request that finds a completed operation can return the stored result; one that finds work still progressing can return its current status; a key collision with a different payload should not mutate the original operation. The provider’s cached-response rules matter here because the original provider response may or may not be replayed.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep idempotency records long enough to cover the retry behavior promised by your API and the provider’s deduplication window. Do not assume provider retention is indefinite: after a provider’s window expires, a repeated provider request may no longer be deduplicated under the documented semantics. Your local record and recovery policy should make that boundary explicit.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.