Engineering · September 21, 2026

Idempotency Keys for Email APIs

Prevent duplicate emails during network retries by implementing idempotency keys. Learn how to handle distributed system failures without spamming your users.

The Duplicate Email Problem

Duplicate emails happen when a client sends a request, the server processes it, but the network fails before the client receives the success response. The client, seeing a timeout or 5xx error, retries the request. Without idempotency, the server treats the retry as a new request and sends the email again. Idempotency keys prevent this by allowing the server to recognize a repeated request and return the original result without re-executing the side effect.

As an engineer owning the incident queue, there is nothing worse than a "duplicate email storm." This usually happens during a partial outage of an upstream provider or a database deadlock that slows down response times. Your retry logic, designed for reliability, becomes a weapon that spams your users and damages your sender reputation.

Why Retries Fail Without Idempotency

In a distributed system, there are three points of failure for any API call:

  1. The request never reaches the server.
  2. The server processes the request but the response is lost.
  3. The server crashes mid-process.

If you retry in case 1, you are safe. If you retry in case 2, you send a duplicate. If you retry in case 3, you might send a duplicate depending on where the crash occurred.

Sending an email is an external side effect. Unlike updating a user's name in a database (which is naturally idempotent if you use SET name = 'Alice'), sending an email is an additive action. Every call to a send endpoint creates a new message in the world. To make this idempotent, you must introduce a unique identifier for the intent to send, known as an idempotency key.

Implementing Idempotency Keys

An idempotency key is a unique value (usually a UUID v4) generated by the client and sent in the request header. The server uses this key to track the state of the request.

The Server-Side Workflow

  1. Receive Request: The server checks if the Idempotency-Key header exists.
  2. Lookup: The server checks a fast-access store (like Redis) for that key.
  3. Cache Hit: If the key exists, the server returns the cached response immediately without calling the email delivery engine.
  4. Cache Miss: The server locks the key, processes the email send, stores the response, and returns it to the client.
  5. Expiration: The key is set to expire after a window (e.g., 24 hours) to prevent the database from growing indefinitely.

Concrete Payload Example

Here is how a request should look when using an API like SendHQ:

POST /v1/send Host: api.sendhq.cc Content-Type: application/json Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Authorization: Bearer YOUR_API_KEY { "to": "user@example.com", "template_id": "welcome-email", "variables": { "name": "Alex" } }

Handling Error Cases

Not all retries should be treated the same. You must distinguish between client errors and server errors.

  • 4xx Errors: If the server returns a 400 (Bad Request) or 422 (Unprocessable Entity), the request is invalid. Retrying with the same key should return the same 4xx error. Do not change the payload and reuse the key, as this creates a conflict.
  • 5xx Errors: If the server returns a 500 or 503, the client should retry. If the server had already successfully handed the email to the MTA (Mail Transfer Agent), the idempotency key ensures the retry returns a 200 OK instead of sending a second email.
  • Concurrent Requests: If two identical requests with the same key arrive at the exact same millisecond, the server should return a 409 Conflict for the second request to indicate that the first is still processing.

Idempotency for AI Agents

AI agents (using MCP servers or A2A cards) introduce a new layer of risk. LLMs can be non-deterministic and may trigger the same tool call multiple times if they perceive a failure in the loop.

When building agent-ready integrations, you should never allow an agent to trigger a send action without an approval step or a deterministic idempotency key generated by the orchestrator. The orchestrator should map the agent's intent (e.g., "Send the weekly report to Bob") to a stable key based on the report ID and the date. This prevents the agent from accidentally sending the same report five times because it "thought" the first call failed.

The Cost of Failure: Provider Comparison

When you fail to implement idempotency, you don't just annoy users; you waste money. While some providers are cheaper, the cost of duplicates scales quickly.

According to official pricing pages (as of September 2026):

  • Amazon SES: Costs 0.10 USD per 1,000 emails a la carte (Amazon SES Pricing). New tiered plans introduced July 21, 2026, include Essentials (0.16 USD per 1,000), Pro (0.22 USD per 1,000 plus 105 USD per month per region), and Enterprise (0.23 USD per 1,000 plus 500 USD per month).
  • Resend: Free tier is 3,000 emails per month capped at 100 per day. Pro is 20 USD per month for 50,000 emails, with overages at 0.90 USD per 1,000 (Resend Pricing).
  • SendGrid: The free tier is now a 60-day trial, and Essentials plans start at 19.95 USD per month (SendGrid Pricing).
  • Mailgun: Costs 15 USD per month for 10,000 emails, with overages ranging from 1.80 to 1.10 USD per 1,000 (Mailgun Pricing).
  • Postmark: Costs 15 USD per month for 10,000 emails, with overages from 1.80 to 1.20 USD per 1,000 (Postmark Pricing).

To put this in perspective, sending 50,000 emails costs about 5 USD on SES a la carte, compared to about 66 USD on Postmark tiers. If a retry loop without idempotency accidentally multiplies your volume by 10x, the financial difference between providers becomes a significant line item in your incident report.

Deliverability vs. Acceptance

It is critical to understand that idempotency only solves the problem of acceptance.

  1. Acceptance: The API accepts your request and returns 200 OK. Idempotency keys live here.
  2. Delivery: The API hands the email to the receiving server (e.g., Gmail). This is where SPF and DKIM/DMARC matter.
  3. Inbox Placement: The receiving server decides if the email goes to the Inbox or the Spam folder.

An idempotency key ensures you only accept the request once. It does not guarantee the email is delivered or that it avoids the spam folder. To ensure your infrastructure is correctly configured for delivery, you should use tools like the SendHQ Email DNS Checker to verify your records.

Implementation Checklist for Engineers

If you are auditing your email sending logic today, use this checklist:

  • Client-side Key Generation: Are you generating a UUID v4 for every unique email intent?
  • Header Implementation: Is the key passed in a standard header (e.g., Idempotency-Key) rather than the request body?
  • Storage Layer: Do you have a TTL (Time To Live) on your idempotency keys to prevent storage bloat?
  • Atomic Locking: Does your server use a distributed lock (like SET NX in Redis) to prevent race conditions on the same key?
  • Response Caching: Are you storing the full response (status code and body) to return to the client on retries?
  • Agent Guardrails: If using AI agents, is the key generated by the system orchestrator rather than the LLM?

Code Example: Node.js Idempotency Middleware

Here is a simplified example of how you might implement this logic in a Node.js environment using Redis.

const redis = require('redis'); const client = redis.createClient(); async function sendEmailHandler(req, res) { const idempotencyKey = req.headers['idempotency-key']; if (!idempotencyKey) { return res.status(400).json({ error: 'Idempotency-Key header is required' }); } // Try to acquire a lock and check for existing response const cachedResponse = await client.get(`idempotency:${idempotencyKey}`); if (cachedResponse) { const { status, body } = JSON.parse(cachedResponse); return res.status(status).json(body); } // Set a lock to prevent concurrent requests const lock = await client.set(`lock:${idempotencyKey}`, 'true', 'NX', 'EX', 30); if (!lock) { return res.status(409).json({ error: 'Request is currently being processed' }); } try { // Actual email sending logic const result = await emailProvider.send(req.body); const responsePayload = { status: 200, body: result }; // Cache the result for 24 hours await client.set(`idempotency:${idempotencyKey}`, JSON.stringify(responsePayload), 'EX', 86400); return res.status(200).json(result); } catch (error) { return res.status(500).json({ error: 'Internal Server Error' }); } finally { await client.del(`lock:${idempotencyKey}`); } }

Final Thoughts

Idempotency is not a "nice to have" for transactional email; it is a requirement for any system that values user experience and cost control. By shifting the responsibility of uniqueness to the client and providing a mechanism to track that uniqueness on the server, you eliminate the risk of duplicate sends during network instability.

Whether you are building a traditional SaaS product or an autonomous AI agent, treating email as a critical side effect ensures your system remains reliable and your users stay happy. For a developer-first email API that handles these complexities, check out https://sendhq.cc.