HinterBuild logoHinterBuild
Learning · 13 min read

Exactly-Once Email Delivery for Courses

Exactly-once email delivery for courses: Postgres skip-locked queues, pre-provider records, and idempotent keys so a retry never double-sends a lesson.

Muhammad Abdul Sami, author

Muhammad Abdul Sami

· 13 min read

  • PostgreSQL
  • Email
  • Learning
  • Architecture

Exactly-once email delivery for courses is the difference between a tutor and a spammer. HinterBuild's Cadensend is an open-source MIT email curriculum engine: it plans a series from your sources, writes cited issues, and delivers each approved issue once. Self-hosted. No hosted signup. MVP sends only to your verified address. Not a marketing suite, CRM, or bulk sender.

This post is the delivery pillar: Postgres as a durable queue, FOR UPDATE SKIP LOCKED claims, delivery rows written before the provider call, and keys on workspace, issue, recipient, and issue version. Networks fail. Workers restart. Email APIs time out after they accepted the message. If your scheduler is an in-memory timer, you will double-send. Clone Cadensend on GitHub. The same primitives appear in our backend API engineering and in the idempotency and skip locked guides.

Key Takeaways:

  • True exactly-once on an unreliable network is a protocol myth; effectively-once side effects are an application design.
  • Write a delivery record before calling the email provider; retries return the original attempt.
  • Claim jobs with FOR UPDATE SKIP LOCKED so replicas cannot send the same issue.
  • Idempotency keys must include issue version or an edit becomes a silent no-op forever.
  • Cadensend MVP: one verified recipient, self-hosted MIT, no list sending.
  • Cadence and timezone are wasted if delivery is not exactly once.

Table of Contents:

What "Exactly Once" Means for Email Courses

Short answer: The learner receives each approved issue version at most once, even when workers crash and HTTP is retried.

Distributed systems literature is blunt: you cannot have perfect exactly-once delivery across an unreliable network. Message brokers give at-least-once. HTTP is at-least-once if the client retries. What you can have is exactly-once processing of a named side effect: "send issue 12 version 3 to this recipient." That is idempotency. Cadensend's metric target is 0 duplicate deliveries tolerated.

For a course, duplicates are not a billing glitch. They train the reader to ignore the series. That destroys email learning cadence even when the timezone math is correct.

Scope reminder: Cadensend does not blast a list. MVP is your verified address. The same idempotency math will matter later for opt-in audiences; it is not an excuse to skip keys today. See the product page. HinterBuild's about describes the studio that ships this class of system.

Content still has to be worth sending. Grounding lives in RAG LLM systems, hallucination, and educational hallucination. Delivery does not fix a fake API.

Failure Modes That Duplicate Lessons

Short answer: Timeouts after accept, overlapping workers, naive "resend," and version-blind keys.

FailureWhat the learner seesRoot cause
Provider timeoutTwo copies minutes apartRetry after 200-on-the-wire that the client never saw
Two schedulersTwo copies at onceNo row lock / no skip locked
Deploy mid-sendDuplicate after restartIn-memory job set
"Just resend" buttonDuplicate of v1New attempt without key
Edit after sendNothing, or a surprise v2Key missing version, or send-on-save
DST double fireExtra dawn emailInstant stored without IANA intent

PostgreSQL documents row locking in SELECT ... FOR UPDATE. SKIP LOCKED lets other workers take the next free job instead of waiting. That is the skip locked pattern we use in production queues.

Email providers (for example Resend's API or SendGrid) are not magically idempotent unless you pass their idempotency key and persist your own delivery id. Treat the provider as an untrusted at-least-once system.

Do not "fix" a missed send by inserting a second job with a new UUID for the same issue version. That is how you get duplicates with unique ids. Fix claiming, not identity.

Postgres Queue With Skip Locked

Short answer: Jobs are rows. Claim is a transaction. Work happens after commit of the claim, with a lease.

Cadensend claims jobs from Postgres rather than Redis-as-a-queue-of-record. Redis is fast and easy to lose. A course send is a business event. Our backend API engineering default for this class of work is: the database is the log.

A typical claim:

  1. Begin transaction.
  2. Select due jobs FOR UPDATE SKIP LOCKED with a limit.
  3. Stamp claimed_at, claimed_by, lease_expires_at.
  4. Commit.
  5. Perform send using the persisted delivery key.
  6. Mark sent or failed with a stable error code.

If the worker dies after claim and before completion, the lease expires and another worker may claim. That is safe only if step 5 is idempotent — which is why the delivery record exists first.

Scheduler claim lag targets in Cadensend's blueprint are aggressive (p95 under a minute). That is an operations number, not a reason to use an in-process setInterval. Multiple app replicas are expected.

This queue is for Deliver, not for embeddings. Ingest and generate are other graphs. Mixing them makes send latency follow OpenAI's tail. Keep embeddings and RAG off the 07:00 path.

Record Before Provider

Short answer: Insert the attempt row in sending (or equivalent) with a unique key, then HTTP, then mark sent.

The dangerous order is: HTTP send, then insert. A crash between them looks like "never sent" and you send again. The safe order is: insert unique delivery, then HTTP, then update provider message id.

If the insert hits a unique violation, load the existing row and stop. That is the cached response pattern from idempotency.

If HTTP succeeds and the update fails, you have a row in sending with no provider id. Reconciliation: query the provider if they support it, or treat unknown as "do not send again" after a human check. Never automatically send a second message because the update failed.

Cadensend's delivery pillar states this explicitly: the record is written before the provider call, so a retry returns the original attempt. Read it on Cadensend.

Timezone-correct jobs still use this path. A correct instant with a duplicate send is a cadence bug. See cadence and timezone.

Keys, Versions, and Editorial Edits

Short answer: workspace_id + issue_id + recipient_id + issue_version.

Without version, an editor fix cannot send — the key already exists. Without issue id, two issues collide. Without recipient, a future multi-recipient world (not MVP) would collapse. Without workspace, tenants collide — Cadensend enforces workspace in repository queries, the same isolation we use in RAG systems.

Editorial workflow: edit subject, regenerate a section, approve. Approval creates or bumps a version. Only approved versions are eligible to schedule. Locking in Plan Studio is the planning analog: approved plans survive regeneration, as described in curriculum design.

MVP recipient is the operator's verified address. Do not add a list of students by stuffing emails into the recipients table. Consent, suppression, and unsubscribe are later, gated work. Open source email course platforms that are actually ESPs have those features; Cadensend is a curriculum engine.

If you generate a 30-day programming series or a personal knowledge series, each issue still gets this key. Curriculum type does not relax delivery.

Cadensend Run Center style dashboard for course email jobs
Cadensend Run Center style dashboard for course email jobs
Figure 1: Jobs move through generating, review, scheduled, sending, and failed with stable error codes and safe retries.

Webhooks, Bounces, and Replay

Short answer: Verify signatures, reject replays, never let a webhook trigger a new send.

Providers will POST "delivered" twice. They will POST "bounce" late. Cadensend's security model: callbacks are signature-checked and replay-protected before they mutate state. A bounce may mark an address unverified. It must not enqueue issue 1 again.

Logging: redact prompts and source text. Keep ids for tracing. You do not want a log aggregator to become a second copy of the learner's notes from a personal knowledge series.

SSRF and size limits belong to ingestion, not to send, but the same "untrusted bytes" stance applies to webhook bodies. HinterBuild backend reviews treat webhook handlers as public untrusted entrypoints.

How Cadensend Wires Plan to Deliver

Short answer: Plan → Ground → Write → approve → schedule → claim → record → provider.

The four pillars on the product page:

Writer tools cannot browse or execute code. Retrieved text cannot register tools. That does not affect SMTP, but it stops a jailbroken note from scheduling extra sends. Hallucination is a write-stage problem; duplicates are a deliver-stage problem. Keep the stages separate.

Compare engines in open source email course platforms. Listmonk and similar are excellent at campaigns. They are not syllabus validators.

Self-host from GitHub. You pay LLM, email API, and compute. No Cadensend cloud account.

Testing Delivery Without Harming Humans

Short answer: Use a provider sandbox or a sink, assert unique keys under concurrency, never test on a real list — there is no list.

Tests we run on this class of queue:

  1. Concurrent claim: two workers, one job, one send.
  2. Timeout after insert: retry does not call provider again.
  3. Version bump: v2 may send; v1 cannot send twice.
  4. Lease expiry: crashed worker, second worker, still one provider call.
  5. DST: job local time stable across a transition.
  6. Unverified recipient: job fails closed with a stable error code.

Do not use production learner inboxes as load tests. MVP has one verified address; that address is still a human.

Load in embeddings or generation tests separately so you do not confuse RAG garbage with send bugs.

When clients need this pattern outside Cadensend — receipts, passwordless links, operational mail — we implement it under backend API engineering. Contact for reliable email course delivery. Studio context: about.

Frequently Asked Questions

Is exactly-once email delivery for courses actually possible?

Effectively-once sending of a named issue version is possible with unique keys, a durable store, and record-before-provider. Perfect network exactly-once is not. Cadensend is designed for zero tolerated duplicates.

Why PostgreSQL instead of a dedicated queue product?

The send is a relational fact you will audit. SKIP LOCKED is enough for this workload. You already persist series, issues, and versions in Postgres. Adding a second broker increases dual-write bugs unless you add an outbox.

Can Cadensend send one issue to thousands of students exactly once?

Not in the current MVP. It sends only to your verified address. Multi-recipient, consent, and suppression are later updates. Do not treat it as a bulk sender.

What happens if I click "retry" on a failed job?

Retry must reuse the delivery key. If the original attempt actually reached the provider, retry returns that attempt. If it failed before the provider, retry may call once. Stable error codes distinguish those cases.

How do issue edits interact with exactly-once?

Edits bump issue_version. Version is part of the key, so a new approved version is a new side effect. Do not silently mutate a sent body and resend under the old key.

Does timezone logic live in the same transaction as send?

Materialize the UTC instant when scheduling. The send transaction should claim a due job, not recompute civil time under a changing clock. See cadence and timezone.

Is there a hosted Cadensend that handles delivery for me?

No. Self-host the MIT repository. There is no hosted signup.

Who should I call if my course emails duplicate today?

Contact HinterBuild. We implement skip-locked queues and idempotent providers as backend API engineering. Related: about, Cadensend.

Conclusion

Exactly-once email delivery for courses is unique keys, skip-locked claims, and a delivery row that exists before SMTP-as-a-service.

  • Treat the provider as at-least-once.
  • Put version in the key.
  • Keep generation off the send path.
  • Stay in MVP: one verified recipient, self-hosted.

Use Cadensend from GitHub, or book a consultation on reliable email course delivery. More at HinterBuild.

Connect with Abdul Sami on LinkedIn.

Free consultation

Book a free consultation call on reliable email course delivery

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

Book a meeting

Keep reading