There's a specific kind of dread that comes with sending email in bulk. Not the sending itself — that part is easy. It's the after: the moment you realize the batch API returned 207 Multi-Status, which is HTTP's way of saying "some of this worked, some of it didn't, good luck figuring out which is which."
I've lived this enough times that when I sat down to build a sample for Telnyx, I knew exactly what I wanted to make: an email campaign that takes care of itself. Not a script I run and watch. Not a cron job I pray fires on time. A thing that exists, remembers what it's doing, and wakes itself up when it needs to.
Here's how it went.
The scenario that made it real
I framed the sample around a community bank sending fraud alerts to cardholders. Every notice has a deadline: the longer a customer goes without confirming or disputing a suspicious charge, the more money is at risk — and the more the bank's reputation for vigilance erodes. So when a batch of fraud alerts goes out and a third of them fail on the first attempt, "we'll retry later" is not an operations plan. It's a bet that someone is watching. In this scenario, nobody is.
That's the problem I wanted to solve: not sending email — lots of things send email — but accountability at the per-message level, and recovery without a human in the loop.
The idea: the campaign is a thing, not a task
The mental shift came from a single sentence in my notes: the actor is the campaign.
Most batch senders are tasks. They run, they finish, they forget. If something fails, the failure lives in a log you'll grep tomorrow, and the retry lives in a cron job you'll debug next week. I flipped it: the campaign is an actor — a durable actor, specifically. A little running thing with its own memory that survives restarts and crashes.
When you submit a batch, the actor is born. It owns the state of every message: sent or failed, how many attempts, what the last error was, which idempotency key it used. All of that lives in durable storage, so when the platform reboots mid-batch — and it will, eventually — the campaign picks up exactly where it left off. The reboot isn't an incident. It's a pause.
The part I'm proudest of
The retry loop. When the initial send comes back with failures, the agent doesn't spin or poll. It schedules a wake-up call with itself — 60 seconds out — and goes to sleep. When it wakes, it retries only the messages that actually failed, each with a fresh idempotency key so every attempt is individually traceable. If they fail again, it waits longer: 60 seconds, then two minutes, then four, capped at five. Messages that exhaust their attempts get marked EXHAUSTED and left alone.
No cron. No external scheduler. No while-loop burning CPU. The agent sleeps and wakes exactly when it says it will.
And when the campaign reaches its final state, the agent texts me. An actual SMS to my phone: "Campaign campaign-2026-03: 98 sent, 0 failed, 2 exhausted." I didn't appreciate how much I wanted this until the first time it happened. The full audit trail is one URL away — GET /campaigns/:id returns every message, every attempt, every key — but the SMS is the part that means nobody has to go looking.
Why Telnyx
I work at Telnyx, so take this with the appropriate salt — but the reason this sample came together the way it did is that the pieces are one platform. Telnyx calls it AI Communications Infrastructure, and the name is more than marketing: the Email API handles the batch send with idempotency support and returns honest per-message results; the Edge Agent SDK gives me the durable actor runtime — persistent state, queues, self-waking schedules — as primitives instead of glue code; and the SMS API closes the loop to a human.
On any other stack, those are three integrations and a state store I have to run myself. Here, the durability is the runtime. The difference shows up in the code: the entire retry engine is a schedule() call and a filter over failed indices.
What I'd tell you to steal
Two things.
First: treat 207 as data. Parse it per message, update state per message, and never retry a batch wholesale — that's how you double-send the messages that actually succeeded.
Second: make retries a property of the thing that failed, not a property of your infrastructure. When the campaign owns its own wake-up schedule, "we'll retry later" stops being a hope and becomes a guarantee with a timestamp.
The full sample — code, smoke test, deploy scripts — is on GitHub. Clone it, break it on purpose, and watch it put itself back together. That part never gets old.
Top comments (0)