One signup bug. Over a million emails.
A user messaged me on WhatsApp: “my inbox is being spammed by your app.” I thought I’d been hacked. I hadn’t. My own code was stuck in a loop.
1.12M emails sent by my email provider in 15 days
15 welcome emails one user got in a single minute
~61% deliverability. Spam folders everywhere
The loop
Signup tried to save the user. The save failed quietly, but the code still sent the welcome email. The app retried the signup, failed again, and sent another one. Every few minutes, for every new user.
The bill
Across my apps I was already paying $300–500 a month just for signup and follow-up emails. The loop pushed my provider into pay-as-you-go overage on top of that.
Lesson: an email is a side effect. Only send it after the thing it’s about (the signup, the payment) has actually been saved. Never inside a retry.
Two guards that stop any email loop.
Once per user
Store a “welcome sent” flag per user. Check it first, set it, then send. A retry finds the flag and stops.
// Cloudflare Worker: welcome email, once per user
const key = `welcome:${userId}`;
if (await env.KV.get(key)) return; // already sent
await env.KV.put(key, "1"); // mark BEFORE sending
await env.EMAIL.send({ to, from, subject, html, text });
A hard cap per inbox
Even if a bug slips through, no one should get more than a few emails an hour from you. I cap my apps at 5 per recipient per hour and 300 a day per app.
// max 5 emails per recipient per hour
const k = `rate:${to}:${Math.floor(Date.now() / 3600000)}`;
const n = Number(await env.KV.get(k) || 0);
if (n >= 5) return;
await env.KV.put(k, String(n + 1), { expirationTtl: 3600 });
Tip →
Paste your signup code into Claude and ask: “Can this send the same email twice? Add a once-per-user flag and a per-recipient cap.”
Cloudflare Email Sending. $0.35 per 1,000 emails.
developers.cloudflare.com/email-service — pricing (Email Sending is in beta, on the Workers Paid plan).
$0.35 per 1,000 emails after that
~$34 for 100,000 emails a month (plus the Workers Paid plan)
Onboard your domain
Dashboard → Compute → Email Service → Email Sending → Onboard Domain. Cloudflare adds the SPF, DKIM, DMARC and bounce MX records for you if the domain is on Cloudflare.
Send from a Worker (or the REST API)
Add the binding to wrangler.toml, then call env.EMAIL.send(). Not on Workers? POST to /accounts/{id}/email/sending/send with an API token.
# wrangler.toml
[[send_email]]
name = "EMAIL"
remote = true
// worker
await env.EMAIL.send({ to: "user@example.com", from: "welcome@yourdomain.com",
subject: "Welcome!", html: "<h1>Welcome!</h1>", text: "Welcome!" });
Mine started at 1,000 a day. I got it to 10,000.
New accounts start with a small daily quota. Mine was 1,000 emails a day, shared by every app on the account. One busy day of signups used it up, and login codes started failing. Adding billing credit did not raise it.
developers.cloudflare.com/email-service/platform/limits
Fill the Limit Increase Request Form
It’s linked at the bottom of the limits page: forms.gle/eX6pXvit1wBv77Yw5. Use the email and account you send from.
Show you’re a clean sender
I asked for 10,000 a day and included my numbers: 98% delivery, 1.98% bounce rate, 0 complaints. Say what you send (OTPs, receipts, onboarding) and that every email is opt-in.
Split critical email from the rest
Login codes should never fail because a welcome campaign used the quota. Cap the nice-to-have emails so OTPs always get through.
Cloudflare for Startups: $10,000 in credits.
cloudflare.com/startups — bootstrapped companies get Tier 3 ($10k, for one year). Review takes up to 48 hours.
Check you qualify
Founded in the last 10 years, a live website, active LinkedIn/X/GitHub, a for-profit company, first-time applicant, and a business email on your own domain.
Apply as a product, not an agency
I got rejected the first time: my homepage read like a dev shop (“we help teams build apps”). I made it product-first, replied with my real apps, users and revenue, and was approved 4 days later.
Know what the credit covers
Usage-based services: Workers, D1, R2, Queues, Workers AI and more. Domain registration and AI Gateway are not covered. It’s a 12-month clock, so move real workloads early.
