HinterBuild logoHinterBuild
Learning · 12 min read

Idempotent Email Sends Explained Simply

Idempotent email sends mean a retried lesson never creates a second copy. Cadensend keys issue, recipient, and version first.

Muhammad Abdul Sami, author

Muhammad Abdul Sami

· 12 min read

  • Idempotency
  • Email Delivery
  • Cadensend
  • Reliability

Idempotent email sends explained simply: if the same send is attempted twice, the learner still receives one copy of that issue version. Networks time out. Workers crash. SMTP says “try again.” Without a key, “try again” becomes lesson 3 in the inbox twice and a complaint. Cadensend is a self-hosted MIT email curriculum engine that writes the delivery record before it calls your provider, keyed on workspace, issue, recipient, and issue version. There is no hosted signup. MVP mail goes to your verified address only.

This is the teaching-facing version of idempotency in distributed systems. HinterBuild built the protocol into Cadensend because a personal syllabus that double-fires trains you to ignore the series — the same way duplicate receipts train users to hit junk. Source: GitHub. Questions: contact.

Key Takeaways:

  • Idempotent send = same logical lesson + same recipient + same version → at most one provider handoff that counts.
  • “At least once” delivery (most queues) is safe only if the side effect is keyed.
  • Write the delivery row before the API call; retries return the original attempt.
  • Cadensend is not a bulk ESP; exactly-once is still required for a one-person course.
  • Changing the issue content should change the version so a corrected lesson can send again on purpose.

Table of Contents:

The Everyday Failure

Short answer: Duplicate course email is usually a retry bug, not a user clicking twice.

You approve issue 4. The scheduler claims the job. Your laptop sleeps in the middle of the HTTPS call to Postmark. You wake the machine. The job looks unfinished. The worker sends again. Two identical lessons. You delete both. Next week you skip the series.

That story does not require 10 million users. It requires a timeout. The large-scale version is in system design for 10 million users; the protocol is the same. Stripe documented the pattern for payments years ago with Idempotency-Key headers (Stripe’s idempotency docs). Email needs the analogue: you cannot “refund” a second MIME.

Cadensend’s design target is zero duplicate deliveries tolerated. Scheduler claim lag under 60s p95, approved-send lag under 2 minutes p95. Those numbers are engineering budgets, not marketing. They exist so a personal PKM → email series feels like a teacher, not a broken cron.

If the body of the lesson was hallucinated, you have two problems. Fix grounding with attached sources and hallucination causes. Idempotency will faithfully send a lie once. That is still better than sending it twice.

Cadensend dashboard with series status rather than a fire-and-forget cron
Cadensend dashboard with series status rather than a fire-and-forget cron

Figure 1: Series state belongs in a durable store, not in an in-memory timer.

Idempotency Keys for Lessons, Not Payments

Short answer: Key on the lesson identity, not on a random UUID that changes every retry.

A payment key is often a client-generated UUID held for the checkout session. A curriculum key should be reconstructable:

workspace_id + issue_id + recipient_id + issue_version

Why version? Because you do want to send again after an editorial fix. Issue 4 v1 went out with a broken diagram. You ship v2. That is a new key. A retry of v1 must not create a third v1.

AttemptKeyResult
First send of issue 4 v1ws/issue4/you/v1Provider accepted; row stored
Worker retry of v1sameReturn stored provider id; no second mail
Editor publishes v2ws/issue4/you/v2New send allowed
Duplicate scheduler tick of v2same v2 keyNo second mail

Recipient is in the key even in MVP (one verified address) so a later opt-in audience cannot reuse “issue 4 v1” across people incorrectly — or can reuse it per person correctly. Cadensend’s later updates add audiences only after consent and suppression exist. Do not improvise a BCC list; that is how you skip both idempotency and spam-filter hygiene.

This is not ConvertKit’s visual automation delay. Kit retries according to its ESP. Ghost hands Mailgun a newsletter. Category differences: ConvertKit vs Ghost vs Cadensend. Cadensend is the engine that treats a lesson like a ledger line.

Write-Ahead Delivery Records

Short answer: Insert “we are sending this key” before the network call. If the call succeeds, store the provider id. If you crash, the next worker sees the row.

The dangerous order is: call provider → crash → no row → call provider again. The safe order is:

  1. Begin transaction. Claim job (FOR UPDATE SKIP LOCKED).
  2. Insert delivery row in a unique index on the key (status sending or equivalent).
  3. Commit so the row survives a crash.
  4. Call the provider with a provider-level idempotency key if they offer one (SES, many HTTP APIs).
  5. Update the row with provider message id / error code.

If step 4 succeeded and step 5 never ran, the next worker must not treat “no message id yet” as “never sent.” It should query the provider or treat unknown-after-success-window as “already attempted” and surface a Run Center error for a human — not blindly resend. Cadensend’s Run Center exists for those stable error codes.

Unique indexes are the real mutex. Application memory is not. This is the same advice as our backend idempotency guide, applied to educational rather than transactional payloads. Receipts and lessons both become spam when duplicated; they still should not share one domain if complaint profiles differ.

Webhooks from the provider (“delivered”, “bounced”) must be signature-checked and replay-protected. Cadensend’s security notes say that explicitly. A replayed webhook should not insert a second delivery row.

Queues, SKIP LOCKED, and Restarts

Short answer: Postgres as a queue beats a setInterval in a Node process that dies on deploy.

SELECT … FOR UPDATE SKIP LOCKED lets multiple workers grab different jobs without double-claiming the same row. When the worker dies, the lock releases; another worker may claim. That is at-least-once claim. Exactly-once side effect still depends on the delivery key. Confusing those two phrases is how people ship bugs.

In-memory schedulers forget timezone intent too. Cadensend stores the UTC instant beside the original IANA zone so DST does not shift existing issues. Pair this post with timezone-correct email scheduling. A “retry because the clock looked wrong” is a common source of duplicates.

Self-hosting on a laptop (self-host an email course) makes crashes more likely: lid close, sleep, Docker Desktop updates. That is an argument for write-ahead keys, not against local hosting.

Retrieval jobs should be idempotent too. Cadensend ingestion is content-hash keyed: fetch, parse, chunk, embed, verify, resumable. If embedding retries duplicate vectors, you get the failure mode in why RAG returns garbage. Embeddings and RAG vs fine-tuning vs prompting are how you think about the Ground pillar. The Deliver pillar is this article.

HinterBuild’s RAG/LLM systems work uses the same claim/key discipline on ingest workers. About is the company; the product remains Cadensend, not a hosted ESP.

When You Want a Second Send

Short answer: Bump version, or send a different issue. Do not delete the unique row and “just this once” retry v1.

Legitimate second mails:

  • New issue in the series (issue 5).
  • Corrected version (v2) after you found a bad citation.
  • A different recipient, when audiences exist.

Illegitimate:

  • Scheduler catch-up after you were offline, replaying v1.
  • “The API timed out so we don’t know” followed by a second call without checking the unique row.
  • Manual button mash in an admin UI without the same key.

Editorial workspace in Cadensend diffs versions. Subjects and preheaders are part of the issue document; changing them without a version bump would either silently mutate a sent lesson (bad) or block a resend (confusing). Treat subject/preheader like content. Craft them well (subject lines, preheaders) before first send.

ChatGPT Projects have no send ledger. You can paste the same answer into Gmail five times. That is why ChatGPT Projects vs email syllabi is a comparison of memory, not of delivery.

How This Differs from Marketing Retries

Short answer: ESPs retry to maximize eventual send. Curriculum engines should maximize exactly one send per version.

Kit and Ghost are allowed to think in campaigns. A campaign that failed at 10% might legitimately continue. A lesson that succeeded at 100% of its one recipient must stop. Cadensend is explicitly not a marketing suite, not a CRM, not a bulk sender. Read the product page before you file a “where is my list?” issue.

Deliverability still matters for the copy that does send. Duplicate identical HTML is a junk fingerprint (avoid spam filters). Grounded citations from sources you supplied keep links alive. The writer has no arbitrary network tool; it cannot wander off to invent a URL that 404s.

If you need a human to review a send pipeline plus retrieval, contact HinterBuild. Engineering lead: LinkedIn. Clone the engine from GitHub — do not hunt for a signup form.

Failure Modes We See in the Wild

Short answer: Most duplicate lessons are not “SMTP is broken.” They are missing keys, inverted write order, or humans bypassing the ledger.

Timeout after 201. The provider accepted the message and returned an id. Your process died before you stored it. The next worker treats the job as fresh. The fix is the unique row inserted before the call, plus a “sending” state that means “do not call again; reconcile.” If you only unique-index on success, you still double-send.

Provider-level retry plus your retry. SES, Postmark, and others may retry internally. If you also generate a new provider idempotency key on every worker attempt, you asked for two MIME objects. Pass a stable provider key derived from the same workspace/issue/recipient/version tuple. Amazon documents this for SendEmail (SES and idempotency is the orientation; use their current idempotency headers when available).

Admin “Send now” next to a scheduled job. Two code paths, one lesson. Both must share the unique index. UI buttons are workers. Cadensend’s Run Center should show one job, not a secret second door.

Version confusion. You edit the body, forget to bump version, and expect a resend. The unique key correctly blocks you. That feels like a bug. It is the product working. Bump version for corrections; do not delete the row.

Partial HTML regeneration. A section regen that does not bump version but still “sends again” is a footgun. Treat the issue document as immutable once the delivery row exists, except via version.

Webhook creating sends. A “delivered” callback must never insert a new delivery. It attaches state to the existing key. Replayed webhooks without signature and nonce checks are how ledgers fork. Cadensend’s security notes require signature-checked, replay-protected callbacks.

Local catcher vs production. Mailpit on a laptop makes duplicates look harmless. They are still logic bugs. When you point at a real provider, the same bug hits spam filters. Test the unique constraint with a chaos click: hit send, kill the worker, hit send.

Clock and DST catch-up. A scheduler that wakes and fires every overdue issue without checking keys will dump a week of lessons. Combine this article with timezone-correct scheduling. Idempotency stops duplicates; it does not stop a burst of different issue ids. Burst policy is separate: send the next unsent issue, re-space the rest.

When we built teaching series internally, the incident that taught us write-ahead was not a 10-million-user outage. It was one engineer’s laptop sleeping during a demo. Two copies of issue 1, zero copies of trust. Cadensend’s 0-duplicate target exists so that demo never ships as a product.

Frequently Asked Questions

What does idempotent email send mean in one sentence?

Retrying the same lesson, recipient, and version does not create a second email; the system returns the original send attempt.

Is Cadensend exactly-once at the SMTP layer?

SMTP is not exactly-once. Cadensend makes the application side effect exactly-once by keying the delivery record and writing it before the provider call. The provider may still retry internally; you pass their idempotency keys when available.

Can I resend a lesson after I fix a typo?

Yes — by publishing a new issue version, which is a new key. Do not delete the old row and replay v1.

Why include recipient in the key if MVP has one address?

So the model stays correct when opt-in audiences arrive later, and so a misconfiguration cannot treat two people as one send.

Does this replace SPF and DKIM?

No. Idempotency stops duplicates. Authentication stops spoofing. You want both; see avoid spam filters for educational email.

How is this different from “just use a cron”?

Cron is at-least-once with no memory after a crash. A unique delivery row is memory. Cadensend uses a durable queue in Postgres, not an in-memory timer.

What if the provider accepted the mail and my database rolled back?

That is the failure the write-ahead pattern is designed to shrink. If it still happens, operators inspect Run Center and the provider dashboard rather than blindly clicking Send.

Conclusion

  • Idempotent email sends mean one logical lesson version reaches a recipient once, even when every layer retries.
  • Put the unique key on workspace, issue, recipient, and version; write the row before the provider call.
  • At-least-once queues are fine; at-least-once side effects are how courses become spam.
  • Cadensend is a self-hosted curriculum engine (MIT, no hosted signup, verified address in MVP) built around that protocol.
  • Version bumps are how corrections ship; retries are not.

Run Cadensend from GitHub, read the deeper idempotency guide, or talk to HinterBuild. Company: /about. Lead: LinkedIn.

Free consultation

Book a free consultation call on idempotent email delivery

30-minute call with the HinterBuild team. Discuss your project, architecture questions, or next steps — no obligation.

Book a meeting

Keep reading