Cloudflare email
Send Email With Cloudflare Workers: The Complete, Plain-English Guide
Sending email from a Cloudflare Worker is, at its core, a single fetch() call. Everything that makes it reliable happens in the steps around that call — verifying a domain, hiding a key, and reading the headers afterward to prove it worked. This is the whole recipe, code and all.
This is the hands-on guide: real Worker code for both ways a Worker can put mail in motion, the exact commands to wire it up, and the one header to check when you're done. If you want the plain-English, no-code version of "Routing receives, now how do I send?", that lives in our guide to sending from Email Routing. Here, we write the Worker.
First, what is a Cloudflare Worker?
A Worker is a small program that runs on Cloudflare's network instead of on a server you own. There's no machine to rent, patch, or keep awake at 3 a.m. You write a bit of code, Cloudflare runs it close to whoever triggers it, and you only pay for the moments it actually runs. People use Workers to build APIs, reshape web requests, run scheduled jobs, and — relevant here — react to email.
There are two flavors worth knowing:
- Regular Workers run when a web request or a timer fires.
- Email Workers run the moment a message arrives through Cloudflare Email Routing. They can read the message, sort it, store it, forward it, or reject it.
Both kinds can send mail — but neither does it by opening a direct connection to the recipient's mail server.
Why a Worker can't just blast email on its own
Sending email the traditional way means talking SMTP — the mail protocol — directly to the recipient's server. Workers deliberately don't do that. Two reasons:
Reputation. Whether an email lands in the inbox or the spam folder depends heavily on the reputation of the server and IP address it came from. A shared, anonymous edge network is not a trusted sender, and making it one would be a never-ending fight against abuse.
Safety. If any Worker could send to anyone, the platform would become a spam cannon overnight. Keeping raw outbound SMTP off the table keeps the whole network clean.
An Email Worker can reply to or forward a message it just received, because that stays within a conversation the sender started. But to send a fresh message to an arbitrary recipient, you need a different path.
Path A — the one request that sends to anyone
This is the workhorse: a plain web request from your Worker to a transactional email provider (Resend, and others in the same category), which does the actual delivery from infrastructure receivers already trust. Your Worker just hands over the envelope.
// A regular Worker: send to any recipient via a provider's HTTP API
export default {
async fetch(request, env) {
const res = await fetch("https://api.your-provider.com/emails", {
method: "POST",
headers: {
// env.MAIL_API_KEY is a stored secret — never hardcode it
"Authorization": "Bearer " + env.MAIL_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
from: "support@yourdomain.com", // a domain you've verified
to: "anyone@example.com", // any recipient on the internet
subject: "Hello from the edge",
text: "Sent by a Cloudflare Worker through a provider API."
})
});
return new Response(res.ok ? "sent" : "failed", { status: res.status });
}
};
The secret is the whole security story. You never paste the key into the file — you store it once, encrypted, and read it at runtime by name:
# stored encrypted; it does NOT appear in wrangler.toml or your code wrangler secret put MAIL_API_KEY
Now env.MAIL_API_KEY exists inside the Worker but never reaches a browser, a log, or your Git history. That one binding is the difference between "a tidy little sender" and "a leaked key in a public repo."
Path B — replying and forwarding, no provider required
There's a second path that never touches a provider. An Email Worker runs the instant a message arrives through Email Routing, and it can act on that message — forward it, or reply in-thread — because that stays inside a conversation the sender already started. Different trigger, different API:
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
export default {
async email(message, env, ctx) {
// Forward a received message to a verified destination address
if ((message.headers.get("subject") || "").includes("invoice")) {
await message.forward("billing@yourdomain.com");
return;
}
// Or reply in-thread — note the In-Reply-To linking back to the original
const mime = createMimeMessage();
mime.setHeader("In-Reply-To", message.headers.get("Message-ID"));
mime.setSender({ name: "Support", addr: message.to }); // answer AS the address it came to
mime.setRecipient(message.from);
mime.setSubject("Re: " + message.headers.get("subject"));
mime.addMessage({ contentType: "text/plain", data: "Thanks — we got your note." });
await message.reply(new EmailMessage(message.to, message.from, mime.asRaw()));
}
};
Three things worth noticing. message.forward() only targets addresses you've verified in Email Routing. message.reply() only works on a message that was actually received, and the reply must carry an In-Reply-To pointing at the original — that's what keeps it a reply and not fresh outbound. (To bounce instead, call message.setReject("reason").) None of this needs a provider, because none of it starts a new conversation.
Which path, by who you're mailing
The choice is almost mechanical:
- Mailing someone who didn't just write to you — a notification, a digest, a reply from a web app — is Path A. Workers can't open raw SMTP, so a fresh message to an arbitrary recipient goes through the provider API.
- Answering or routing a message that just arrived is Path B. Forward and reply stay in-conversation, so Email Routing handles them directly, no third party involved.
End to end: verify, store, send, prove it landed
Here's the smallest loop that proves the whole thing works, from nothing to a trusted message:
- Verify the domain. In your provider's dashboard, add your sending domain and paste the records it gives you into DNS — an SPF entry (who may send), a DKIM key (a signature on every message), and a DMARC policy (what to do on failure). If the domain already lives on Cloudflare, that's a few rows in the same dashboard.
- Store the secret. Run wrangler secret put MAIL_API_KEY and paste the provider key when prompted. It's now encrypted, not in your code.
- Send. wrangler deploy the Path A Worker and hit its URL, with from set to your verified domain and to set to an inbox you can open.
- Prove it. Open that message, view its raw source, and find the Authentication-Results header. You're looking for spf=pass dkim=pass dmarc=pass. Paste the headers into our email header analyzer to read them in plain English, or confirm the records themselves with the SPF, DKIM and DMARC checker. A clean dkim=pass means the provider signed with your domain's key — the actual proof that receivers will treat the mail as genuinely yours.
Pitfalls to avoid
A Worker that sends mail is a handful of lines, but a few missteps turn "it works on my screen" into "it's in everyone's spam":
- Sending from an unverified domain. Your provider won't deliver for a domain it can't confirm you own. Verify the domain and set up its records before your first send.
- Ignoring the provider's rate limits. Transactional APIs cap how fast and how much you can send. Build in a little patience — retries and queuing — rather than firing thousands of requests at once.
- Mismatched "from" and authentication. The domain in your "from" address has to be the one that passes SPF and DKIM. If they don't line up, DMARC-strict receivers may reject the mail — which is exactly what the dkim=pass check in step four is there to catch.
Get these right and a Worker becomes a dependable little mail sender you can forget about — which is exactly what you want from infrastructure.
The honest limits
Keep three things in mind. First, you depend on the provider. The Worker is yours, but delivery belongs to the service you chose. That's a feature — they're better at it than you'd be — but it's a relationship, not magic. Second, transactional isn't bulk. These providers are tuned for real, wanted mail, not mass marketing blasts; abusing them gets you rate-limited or cut off. Third, deliverability needs upkeep. Authentication gets you in the door, but sending clean, wanted mail is what keeps you there.
Within those bounds, "a Worker plus a sending API" is one of the cleanest ways to add email to anything you build — no mail server, no servers at all.
- Can a Cloudflare Worker send email without any third-party service?
- Not to arbitrary recipients. Workers can't make raw SMTP connections, so sending real outbound mail means calling a provider's API. An Email Worker can reply to or forward a message it received.
- Do I need to know how to code?
- To wire it up from scratch, a little. The individual steps are small, but if you'd rather skip the plumbing, an app that already sends through this pattern does the wiring for you.
- Is the API key safe inside a Worker?
- Yes, if you store it as an encrypted secret rather than pasting it into the code. The Worker reads it at runtime; it isn't exposed to visitors.
- Will mail sent this way reach the inbox?
- With SPF, DKIM and DMARC set up and a reputable provider doing the delivery, yes — it's treated as legitimate, authenticated mail.