Skip to content

tutorials

Set up a catch-all address with Cloudflare Email Routing

July 13, 2026

#cloudflare-email-routing#catch-all#workers#email#tutorial
Set up a catch-all address with Cloudflare Email Routing
Visual: Spin Cycle on okslop

You want every address at your domain (sales@, billing@, that one typo a customer used once) to land somewhere instead of bouncing, without creating each address by hand. Cloudflare Email Routing has a catch-all for exactly this.

A catch-all address is one rule that matches every local part at a domain that doesn't already have an explicit rule. Instead of creating support@, hello@, billing@, careers@, and the dozen aliases you'll think of later, you set one wildcard and everything lands in the same place. Cloudflare Email Routing ships this as a first-class feature, and the basic version takes about five minutes. The interesting version, where the catch-all feeds a Worker and you keep the mail, takes a little longer. Here's both.

How do I set up a catch-all address on Cloudflare?

Enable Email Routing on your domain, verify a destination address, then turn on the Catch-all rule under Routing rules and point it at that destination. Every address with no explicit rule is forwarded there. The full path:

Step 1: Enable Email Routing

In the Cloudflare dashboard, select your domain, then go to Email > Email Routing and start setup. For a zone Cloudflare already manages, it adds the MX and TXT records for you. The records it publishes look like this:

MX   @   route1.mx.cloudflare.net   (priority 13)
MX   @   route2.mx.cloudflare.net   (priority 86)
MX   @   route3.mx.cloudflare.net   (priority 24)
TXT  @   v=spf1 include:_spf.mx.cloudflare.net ~all

Important constraint: these must be the only MX records for the domain. Email Routing intercepts mail at Cloudflare's edge, so if you still have Google or another provider's MX records present, delivery is ambiguous and Cloudflare will warn you. One mail destination per domain.

Step 2: Verify a destination address

Under Destination addresses, add the mailbox you want mail forwarded to (your real Gmail, a team alias, whatever) and click the verification link Cloudflare emails to it.

This step bites people. An unverified destination is not an error you'll see at send time; mail to it is simply dropped. If your catch-all "isn't working" later, this is the first thing to check.

Step 3: Enable the catch-all rule

Under Routing rules, find Catch-all address and enable it. Pick an action:

  • Send to an email: forward every catch-all match to a verified destination. This is the whole job for the simple case.
  • Drop: silently discard non-matching mail (useful if you only want explicit addresses to work but don't want bounces).
  • Send to a Worker: hand the message to code you control. This is where it gets interesting.

That's it for the simple case. Mail to anything@yourdomain.com now lands in your verified inbox.

How does precedence work between custom addresses and the catch-all?

Explicit custom addresses always win over the catch-all. Cloudflare evaluates a specific rule for the exact local part first; only if none matches does the catch-all apply.

So a common, clean setup is:

support@yourdomain.com   ->  Send to support-team@gmail.com   (custom)
billing@yourdomain.com   ->  Send to accounting@gmail.com     (custom)
*@yourdomain.com         ->  Send to founder@gmail.com        (catch-all)

support@ and billing@ go to their teams; everything else, including typos and addresses you never set up, falls through to the founder. You get coverage without enumerating every address.

How do I route the catch-all to a Worker?

Set the catch-all action to Send to a Worker and give your Worker an email() handler. Cloudflare invokes it once per matching message with a ForwardableEmailMessage. This is the upgrade from "forward to my Gmail" to "do something with the mail."

Here's a minimal Worker that logs every inbound address and conditionally forwards. Create wrangler.jsonc:

{
  "name": "inbound-mail",
  "main": "src/index.ts",
  "compatibility_date": "2026-06-01",
  "send_email": [
    {
      "name": "FORWARD",
    },
  ],
}

And src/index.ts:

export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const to = message.to;          // e.g. "anything@yourdomain.com"
    const from = message.from;      // envelope sender
    const local = to.split("@")[0]; // "anything"

    // Branch on the local part however you like.
    if (local === "abuse" || local === "postmaster") {
      // RFC-mandated addresses: forward to a human.
      await message.forward("ops@yourdomain.com");
      return;
    }

    if (local.startsWith("noreply")) {
      // Silently accept and drop.
      return;
    }

    // Everything else: forward to the catch-all human.
    await message.forward("founder@yourdomain.com");
  },
};

Deploy it:

npx wrangler deploy
Uploaded inbound-mail (1.42 sec)
Published inbound-mail (2.88 sec)
  https://inbound-mail.yourdomain.workers.dev
Current Deployment ID: a91c...

Then, back in the dashboard, set the catch-all action to Send to a Worker and select inbound-mail. Now every catch-all match runs your code.

Reading the raw message

message.forward() is convenient, but the reason to use a Worker is usually that you want the content. The raw MIME is on message.raw as a stream:

export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const raw = await new Response(message.raw).text();

    // raw is the full RFC 822 message: headers + body + any MIME parts.
    // Pull a header without a full parser:
    const subject = raw.match(/^Subject:\s*(.*)$/m)?.[1] ?? "(no subject)";

    console.log(`Inbound to ${message.to}: ${subject}`);

    // Store it, queue it, hand it to your app...
    await message.forward("founder@yourdomain.com");
  },
};

What are the failure modes to watch for?

Most catch-all problems are one of four things: MX records, the rule being disabled, an unverified destination, or trying to forward to an unverified target from a Worker. Here are the errors you'll actually see.

No mail arrives at all. Check your MX records. Email Routing's MX must be the only MX on the domain. A leftover MX @ aspmx.l.google.com from an old Google Workspace setup will quietly steal your mail. Cloudflare flags this in the Email Routing UI under a "DNS records" warning.

The Worker forward silently fails. When a Worker calls message.forward("x@somewhere.com"), that destination must also be a verified destination address in Email Routing. You'll see this in wrangler tail:

Email handler error: destination address not verified: founder@yourdomain.com

Add and verify the address under Destination addresses, then redeploy logic that targets it.

Mail to a specific address isn't hitting the catch-all. Check for an explicit custom rule on that local part. Custom rules win. If support@ has its own rule, it will never fall through to the catch-all, even if that rule is misconfigured.

You expected to be able to reply, and can't. This is the real ceiling of Email Routing, and it's worth being honest about. Email Routing is inbound only. It receives and forwards; it does not let you send from anything@yourdomain.com. Workers cannot open outbound port 25 for direct-to-MX SMTP, so the catch-all gives you a place for mail to land but not a way to answer it. If you reply from your personal Gmail, the reply comes from Gmail, not your domain. For the why behind that wall, see why Workers can't send email over SMTP.

When the catch-all isn't enough: real inboxes

The catch-all plus a Worker gets you a long way: every address lands somewhere, and you can branch, log, and store the raw message. But you'll notice you've built half an inbox. You can receive, but you can't reply from the domain. You're forwarding to a personal mailbox, which means the mail isn't really yours, it's Google's again. And if you want to search it, classify it, or let an app act on it, you're back to parsing MIME by hand and standing up storage.

This is the gap the Tinbox email stack closes. Tinbox deploys to your own Cloudflare account and wires the inbound side onto Email Routing for you: the catch-all lands in a real inbox backed by D1 and R2, with the raw message stored, MIME parsed, threads assembled, and full-text search over everything. Crucially it also brings the half Email Routing is missing, an HTTPS-based outbound sender, so you can actually reply from anything@yourdomain.com with proper DKIM signing.

Setup is one command, and it provisions the routing, storage, and sender together:

npx tinbox init yourdomain.com
✔ Enabled Cloudflare Email Routing on yourdomain.com
✔ Configured catch-all -> inbound Worker
✔ Provisioned D1 database (tinbox-yourdomain-db)
✔ Provisioned R2 bucket (tinbox-yourdomain-mail)

Add these DNS records to yourdomain.com:

  MX   @                 route1.mx.cloudflare.net  (priority 13)
  TXT  @                 v=spf1 include:_spf.mx.cloudflare.net ~all
  TXT  tinbox._domainkey v=DKIM1; k=rsa; p=MIIBIjANBg...

Once DNS propagates, run: npx tinbox doctor

From there, every address at the domain is a working two-way inbox, and you also get the newsletter and transactional-sending pieces on the same data. The catch-all you just built by hand is the foundation; Tinbox turns it into the rest of the product.

Where to go from here

Deploy this yourself: npx tinbox init yourdomain.com. Full configuration options are in the docs.

Frequently asked questions

How do I set up a catch-all email address on Cloudflare?

In the Cloudflare dashboard, open your domain, go to Email > Email Routing, and under Routing rules enable the Catch-all address. Set its action to either Send to an email (a verified forwarding address) or Send to a Worker. Once your MX and TXT records are in place and the catch-all is enabled, every address at the domain that has no explicit rule is delivered to that destination.

What is the difference between a custom address and a catch-all in Cloudflare Email Routing?

A custom address is one explicit mapping, like support@yourdomain.com to a forwarding address. A catch-all matches every local part that has no explicit rule, so anything@yourdomain.com is covered by a single rule. Explicit custom addresses always take precedence over the catch-all when both match.

Can Cloudflare Email Routing catch-all forward to a Worker?

Yes. Set the catch-all action to Send to a Worker and bind an email handler in your Worker. The runtime invokes your Worker's email() handler with a ForwardableEmailMessage for every catch-all match, so you can store the raw MIME, parse it, or forward it on conditionally.

Does Cloudflare Email Routing let me send email too?

No. Email Routing is inbound only: it receives mail for your domain and forwards it or hands it to a Worker. It cannot send arbitrary outbound mail, and Workers cannot open outbound port 25 for direct-to-MX SMTP. Outbound sending needs a separate HTTPS-based sender.

Why is my Cloudflare catch-all not receiving any mail?

The usual causes are missing or wrong MX records (Email Routing publishes its own MX and they must be the only MX for the domain), Email Routing not enabled, the catch-all rule left disabled, or a destination forwarding address that was never verified. Mail to an unverified destination is dropped.