Loading course…

Read as text

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.

I sent 1M emails by mistake: move app emails to Cloudflare — free guide by Sanskar Tiwari - Sanskar Tiwari