Cloudflare Email Routing, and what you still have to build
Cloudflare Email Routing is free and forwards mail well. The gap opens when your app needs parsed bodies, stored attachments and threaded replies.
Cloudflare Email Routing’s documentation and pricing last read 30 July 2026
The short version
- Cloudflare Email Routing is free, forwards well, and is the right answer if a person reads the mail. Genuinely — start there.
- The gap is not receiving the message; it is MIME parsing, attachment storage, retries, threading and sending, all of which become code you own inside a Worker.
- It requires your domain to be on Cloudflare's nameservers. Inboundr needs two records in whatever zone you already have.
- Running both is common: free forwarding for human addresses, a subdomain on Inboundr for the machine ones.
What Cloudflare Email Routing actually gives you
If your domain is on Cloudflare, Email Routing is a checkbox away and it costs nothing. You create rules mapping addresses on your domain to destination addresses you have verified, Cloudflare publishes the MX records for you, and mail starts flowing. There is also a catch-all, and there are Email Workers — a Worker invoked with the inbound message instead of an HTTP request, which can forward it, reject it, or reply to it.
It is a genuinely good piece of free infrastructure, and for a large number of people reading this page it is the correct answer. We would rather say that in the first paragraph than have you discover we left it out.
The reason there is a comparison to make at all is that “a Worker can receive the message” and “your application can work with the mail” are separated by a surprising amount of engineering, and the gap is invisible until you are in it.
The Worker you end up writing
An Email Worker hands you the message as a stream. Here is where that starts, and it looks encouragingly short:
export default {
async email(message, env, ctx) {
// message.from, message.to and message.headers are available.
// message.raw is a ReadableStream of the full RFC 5322 message.
await fetch("https://your-app.example.com/hooks/inbound", {
method: "POST",
body: message.raw,
});
},
};That works, and it is also the moment the problem moves rather than gets solved: your application now receives raw MIME over HTTP and has to parse it. So the parsing moves into the Worker, and the list of things you own grows:
- A MIME parser in the Worker. Multipart bodies, quoted-printable and base64 encodings, character sets that are not UTF-8, HTML alternatives, inline images referenced by Content-ID. Real mail from real clients breaks parsers in ways test fixtures do not.
- Attachment storage. A webhook body is the wrong place for a 20 MB PDF, so attachments go to R2 — which means bucket lifecycle rules, key naming, and signed download URLs with expiry.
- Retries, with state. If your endpoint is down, the Worker has one chance. Doing better means a queue, a backoff schedule and somewhere to keep the message meanwhile, because the message itself is not retained anywhere.
- Threading. Grouping a reply with the message it answers means storing Message-ID, In-Reply-To and References and resolving them — a table, an index and the edge cases where a client sends neither header.
- Sending, separately. Replying to the message currently being handled is supported. Replying an hour later, or sending anything not in reply to a live invocation, needs a different provider entirely — so you are back to SES, Resend or Postmark for the outbound half, with its own DKIM setup.
- Spam decisions. Whether to act on a message that failed SPF or looks like spam is a judgement your Worker has to make, from the headers, on its own.
The honest framing
None of this is hard, exactly. It is a week or two of work, and then it is a component in your system that handles untrusted input from the open internet and has to keep working while you are busy with the product. The question is not whether you could write it. It is whether it should be yours.
Side by side
Where the Cloudflare column says partly, it almost always means “possible, in a Worker you write and maintain”. That is a real answer, not a dodge — for some readers it is the preferred one.
| What you need | Inboundr | Cloudflare Email Routing |
|---|---|---|
| Price for a low-volume domain | A free plan with a small monthly message allowance and a short retention window; paid plans past that. | Free, with no message allowance to run out. This is a genuine advantage and no amount of feature comparison changes it. |
| Works with your existing DNS provider | Two records — MX and DKIM — added wherever your zone already lives. Registrar and nameservers stay where they are. | The domain has to be on Cloudflare, using Cloudflare's nameservers. For many people that is already true; if it is not, this is a migration of your whole zone, not two records. |
| Forward to a mailbox a person reads | A forwarding endpoint re-sends the original message to one address or a group. | Per-address rules and a catch-all, forwarding to destination addresses you verify first. Solid, and the reason most people turn it on. |
| The parsed message delivered to your app | An HMAC-signed webhook with subject, text and HTML bodies, addresses, threading ids, attachment metadata and spam and SPF/DKIM verdicts, already extracted. | An Email Worker receives the message as a raw stream. Getting fields out of it means running a MIME parser inside the Worker, within its CPU and memory limits, and maintaining that yourself. |
| Messages stored and fetchable later | Every message is stored for the plan's retention window and can be listed, fetched and re-delivered over the REST API. | Nothing is retained. If your Worker errors, or your service was down when it ran, the message is gone — unless the Worker itself wrote a copy to R2 or D1, which is code you own. |
| Attachments as fetchable objects | Stored individually and handed out as short-lived download links, so a 20 MB PDF never has to travel through your webhook body. | Extract them from the raw MIME in the Worker and put them somewhere yourself. Buckets, keys, lifecycle and download URLs are all your build. |
| Send a threaded reply from code | Name the message you are answering and the threading headers are written for you. Replies go out authenticated and signed with your domain's DKIM. | A Worker can reply to the message it is currently handling. There is no general sending API, no way to reply to a message from an hour ago, and no thread to reply into. |
| Conversations grouped into threads | Messages are threaded on arrival from Message-ID, In-Reply-To and References, so an agent gets the conversation rather than a stack of unrelated mail. | No concept of a thread. Each invocation sees one message with no memory of the last one. |
| An AI agent that can work its own inbox | A hosted MCP server on the same API key: list, read and reply as tool calls, no glue code. | Not offered. You would be building the store, the API over it and the MCP server on top of Workers, R2 and D1. |
| Visibility when something breaks | The console shows the parsed message, every delivery attempt, the response your endpoint gave and the reason a retry is queued. | Routing status in the dashboard and Worker logs through Cloudflare's own tooling. Correlating a sender's 'I emailed you' with a specific invocation is the work. |
Price for a low-volume domain
- Inboundr
- A free plan with a small monthly message allowance and a short retention window; paid plans past that.
- Cloudflare Email Routing
- Free, with no message allowance to run out. This is a genuine advantage and no amount of feature comparison changes it.
Works with your existing DNS provider
- Inboundr
- Two records — MX and DKIM — added wherever your zone already lives. Registrar and nameservers stay where they are.
- Cloudflare Email Routing
- The domain has to be on Cloudflare, using Cloudflare's nameservers. For many people that is already true; if it is not, this is a migration of your whole zone, not two records.
Forward to a mailbox a person reads
- Inboundr
- A forwarding endpoint re-sends the original message to one address or a group.
- Cloudflare Email Routing
- Per-address rules and a catch-all, forwarding to destination addresses you verify first. Solid, and the reason most people turn it on.
The parsed message delivered to your app
- Inboundr
- An HMAC-signed webhook with subject, text and HTML bodies, addresses, threading ids, attachment metadata and spam and SPF/DKIM verdicts, already extracted.
- Cloudflare Email Routing
- An Email Worker receives the message as a raw stream. Getting fields out of it means running a MIME parser inside the Worker, within its CPU and memory limits, and maintaining that yourself.
Messages stored and fetchable later
- Inboundr
- Every message is stored for the plan's retention window and can be listed, fetched and re-delivered over the REST API.
- Cloudflare Email Routing
- Nothing is retained. If your Worker errors, or your service was down when it ran, the message is gone — unless the Worker itself wrote a copy to R2 or D1, which is code you own.
Attachments as fetchable objects
- Inboundr
- Stored individually and handed out as short-lived download links, so a 20 MB PDF never has to travel through your webhook body.
- Cloudflare Email Routing
- Extract them from the raw MIME in the Worker and put them somewhere yourself. Buckets, keys, lifecycle and download URLs are all your build.
Send a threaded reply from code
- Inboundr
- Name the message you are answering and the threading headers are written for you. Replies go out authenticated and signed with your domain's DKIM.
- Cloudflare Email Routing
- A Worker can reply to the message it is currently handling. There is no general sending API, no way to reply to a message from an hour ago, and no thread to reply into.
Conversations grouped into threads
- Inboundr
- Messages are threaded on arrival from Message-ID, In-Reply-To and References, so an agent gets the conversation rather than a stack of unrelated mail.
- Cloudflare Email Routing
- No concept of a thread. Each invocation sees one message with no memory of the last one.
An AI agent that can work its own inbox
- Inboundr
- A hosted MCP server on the same API key: list, read and reply as tool calls, no glue code.
- Cloudflare Email Routing
- Not offered. You would be building the store, the API over it and the MCP server on top of Workers, R2 and D1.
Visibility when something breaks
- Inboundr
- The console shows the parsed message, every delivery attempt, the response your endpoint gave and the reason a retry is queued.
- Cloudflare Email Routing
- Routing status in the dashboard and Worker logs through Cloudflare's own tooling. Correlating a sender's 'I emailed you' with a specific invocation is the work.
When Cloudflare Email Routing is the better choice
- You want forwarding, and nothing else, for free. Addresses on your domain landing in your existing mailbox, at no cost, with your DNS already on Cloudflare. There is no argument to make against that and we are not going to invent one.
- Your domain is already on Cloudflare and you value one vendor. DNS, CDN, Workers, R2 and mail routing on one bill and one dashboard is a real operational simplification.
- You want to own the pipeline. Some teams genuinely prefer their mail handling to be code in their repo, deployed with everything else, with no third party in the path. Email Workers is a good place to do that.
- Volume is high and the processing is trivial. If you receive a great deal of mail and all you do is check one header and drop most of it, per-message pricing is the wrong shape and a free Worker at the edge is the right one.
Using both together
These are not mutually exclusive, and the combination is a reasonable destination rather than a compromise. Keep your zone on Cloudflare — Email Routing and an inbound MX record for the same domain are the one thing that does conflict, since a domain has one set of MX records and they point at one receiver.
So the split is per domain or per subdomain:
- Cloudflare Email Routing on
yourdomain.comfor the handful of human addresses — hello, billing, you. - Inboundr on
mail.yourdomain.comor a dedicated domain for the machine addresses — ticket intake, agent inboxes, per-customer reply addresses. DKIM is per domain, so the subdomain signs independently and a reputation problem on one does not touch the other.
Plenty of accounts run exactly this way. Nobody should move their personal mail off free forwarding to make a comparison page tidy.
Moving an address across
Because a domain has one MX record set, moving is a cutover rather than a gradual roll. The sequencing below avoids a window where mail bounces:
- Decide the hostname first. If any address on the domain should stay on Cloudflare forwarding, use a subdomain for us — that turns a cutover into an addition, and step four stops being risky.
- Add the domain or subdomain in the console and publish the DKIM record in Cloudflare DNS. This does not affect delivery, so it is safe while Email Routing is still live.
- Create the endpoint that will receive the mail — a webhook, or a forward to the same mailbox Cloudflare was delivering to if you want a like-for-like swap first.
- Lower the TTL on the existing MX record, wait for the old value to expire, then replace it with ours. Disable the Email Routing rules afterwards, not before.
- Send a test message and confirm it arrives at the endpoint. The console shows the parsed message and the delivery attempt.
- Keep the Cloudflare rules disabled but present for a week. Turning them back on and restoring one MX record is a five-minute rollback.
Questions people ask
Can I keep my DNS on Cloudflare and still use Inboundr?
Yes, and most people do. We need two records in your zone; whose nameservers serve that zone is irrelevant to us. The constraint runs the other way — Email Routing requires the domain to be on Cloudflare, we do not.
Is an Email Worker not basically a webhook?
It is the delivery mechanism, not the payload. A webhook from us arrives as parsed JSON with bodies, verdicts and attachment links, signed, retried, and backed by a stored copy you can fetch again. An Email Worker arrives as a byte stream with one attempt and no storage. The difference is everything between those two sentences.
Can Cloudflare store messages in R2 for me?
A Worker you write can write to R2 — Cloudflare does not do it for you, and R2 holds bytes rather than a queryable mailbox. “List every message from this sender this week with its attachments” is a schema, an index and an API over that bucket. That is the layer being compared here.
I want an AI agent to handle the mail. Does that change the answer?
It sharpens it. An agent needs the message reduced to text it can reason about, the conversation it belongs to, and a way to reply in-thread — which is the whole list of things an Email Worker does not give you. It also needs guardrails, because an inbox is an unauthenticated write into a model’s context; our guide to giving an agent its own address covers those in detail.
What about sending?
Email Routing is receive-only apart from replying to a message a Worker is currently handling. If your application needs to send — replies later, notifications, anything outbound — that is a separate provider and a separate DKIM setup. Inbound and outbound on one domain identity is part of what you are choosing between.
Would I be better off with a raw cloud mail receiver instead?
Possibly — for an application workload it is a fairer comparison than Cloudflare, because you get the raw message and full control over what happens next. It is also the same list of things to build as the Worker above, minus the edge runtime: parsing, storage, retries, threading, and a separate outbound path with its own bounce handling. The choice is not really which cloud receives the mail; it is whether that layer should be yours to maintain.
Claims about Cloudflare Email Routingwere checked against Cloudflare’s public documentation for Email Routing and Email Workers. Read on 30 July 2026. If something here is out of date, tell us and we will correct it.