Idempotent email delivery on Cloudflare Workers + Durable Objects. Adds an Idempotency-Key dedup layer on top of the Cloudflare Email Service — the request body is fully compatible with the CF Email Sending REST API.
AWS SNS provides a MessageDeduplicationId for deduplication. Cloudflare does not. This project fills that gap.
When do duplicate emails happen? The email was already delivered, but the response was lost — typically a TCP FIN packet dropped during connection teardown. The client can't distinguish "server never got it" from "server got it but I missed the ACK," so it retries.
Why is that a problem?
- Extra cost — each redundant send consumes email provider quota and incurs per-message charges.
- Degraded user experience — a customer receiving the same order status update twice is confusing and erodes trust.
Why not Cloudflare KV? KV is eventually consistent for reads — a duplicate request hitting a different edge node could read a stale (or absent) key before the first write propagates. Durable Objects are single-threaded and single-context, guaranteeing strong consistency per key. Under the PACELC principle, we sacrifice latency for consistency here.
Your Application
│ POST /accounts/{account_id}/email/sending/send + Idempotency-Key: <unique-key>
▼
Cloudflare Worker
├── Durable Object (1 per key): first → mark + send, duplicate → 409
└── Email delivery via CF Email Service (internal RPC)
git clone https://github.com/meow-developer/cf-reliable-email-service.git
cd cf-reliable-email-service
npm install
npm run deploy # wrangler deploy- Fork the repo.
- Add repository secrets —
CLOUDFLARE_API_TOKEN(Workers Scripts: Edit) andCLOUDFLARE_ACCOUNT_IDunder Settings → Secrets and variables → Actions. - Uncomment the
on: pushtrigger and the "Deploy to Cloudflare Workers" step in.github/workflows/deploy.yml.
Pushes to main will then run tests and deploy automatically.
All configuration is in wrangler.toml:
# Dedup key TTL in milliseconds (default: 300000 = 5 min)
[vars]
DEDUP_TTL_MS = "300000"Authentication (optional): Handled at the Cloudflare edge by ZeroTrust Access — zero auth code in the Worker. Create an Access Application + Service Token, then send CF-Access-Client-Id and CF-Access-Client-Secret headers.
This service is fully compatible with the official cloudflare-typescript SDK and the CF Email Sending REST API — no wrapper library or adapter needed. Point the SDK at your Worker URL and pass the Idempotency-Key header via the standard options argument.
import { Cloudflare } from "cloudflare";
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
baseURL: "https://your-worker.workers.dev", // ← your Worker, not api.cloudflare.com
});
await client.emailSending.send(
{
account_id: "your-account-id",
from: "noreply@example.com",
to: "recipient@example.com",
subject: "Hello from cf-reliable-email-service",
html: "<h1>Hello!</h1>",
},
{
headers: { "Idempotency-Key": crypto.randomUUID() }, // ← dedup key
},
);See example/ for a complete runnable demo, including duplicate detection.
The Cloudflare REST API path. Compatible with the official cloudflare-typescript SDK — point baseURL at your Worker and pass the Idempotency-Key header. The account_id path parameter is accepted but unused (the Email binding is account-scoped at the infra level).
| Header | Required | Description |
|---|---|---|
Idempotency-Key |
Yes | Unique key per logical request. Duplicate within TTL → 409. |
Content-Type |
Yes | application/json |
Body: the full CF Email Sending REST API payload — from, to, cc, bcc, reply_to, subject, html, text, headers, attachments. All fields pass through verbatim.
curl -X POST https://your-worker.workers.dev/accounts/$ACCOUNT_ID/email/sending/send \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"from": "sender@example.com",
"to": ["recipient@example.com"],
"subject": "Monthly Report",
"html": "<h1>Hello</h1>",
"text": "Hello"
}'All responses follow the Cloudflare API envelope:
| Status | Description |
|---|---|
200 |
Sent. result = { "delivered": [...], "message_id": "...", "permanent_bounces": [], "queued": [] } |
409 |
Duplicate Idempotency-Key within TTL. errors[0].code = 19999 |
400 |
Missing Idempotency-Key header or invalid body. errors[0].code = 10001 |
500 |
Email send failed. errors[0].code = 10002 |
See example/ for a runnable demo using the official Cloudflare SDK.
Returns { "status": "ok" }.