Cloudflare Email Service vs an Agent Inbox

9 min read · Reviewed by Email for Agents Engineering · Cloudflare Email Service checked

Cloudflare Email Service is the most credible DIY base for agent email: cheap at volume, your own domain today, code at the edge. It hands you transport and routing. Parsing, storage, threading, safe retries, per-agent grants, approval and audit are yours to build. We ship those; we lose on price and domains.

At a glance

Dimension Email for Agents Cloudflare Email Service
Status Live on agents.emailforagents.ai. Paid plans are proposed and not purchasable Email Routing is available on Workers Free and Paid. Email Sending has been in public beta since 16 April 2026, on Workers Paid
Free tier For inbound volume on a free plan, Cloudflare wins outright. One live inbox and 100 email units a month per verified owner identity Workers Free: inbound routing with no charge or volume cap; sending to arbitrary recipients not available. Sends to verified destination addresses are free on all plans
Paid sending They win on price at any volume. No plan can be bought yet Workers Paid ($5 a month account minimum): 3,000 emails included, then $0.35 per 1,000
Your own domain They win this row. Requires a paid plan, and paid plans are not purchasable yet Required to send to arbitrary recipients, and the domain must use Cloudflare DNS. Onboarding adds the SPF, DKIM and DMARC records
Inbound storage Stored threads and messages per inbox, readable over REST and MCP A Worker receives a raw MIME stream per message. Storage is whatever you write to R2, D1 or a Durable Object
Retry safety Idempotency-Key on sends; a reused key with different content fails with idempotency_conflict No idempotency key is documented on the Workers binding or the REST send endpoint
Per-agent permissions Their binding allowlist is a platform-enforced recipient limit we do not mirror one-for-one. 32 named scopes, credentials pinned to inbox ids, MCP tool list filtered by grant Per-binding sender and recipient allowlists; account API tokens with permission to send emails
Human approval An approval resource, pending for up to seven days, emitting approval.requested and approval.rejected No approval primitive in the Email Service docs. The open-source Agentic Inbox app requires explicit confirmation before sending
Test mode Non-routable .mail.invalid inboxes with a simulated transport and an efa_test_ key prefix wrangler dev simulates the send binding and logs to the console, and accepts simulated inbound mail at a local endpoint; remote bindings send real mail

Choose Cloudflare Email Service if

  • You already run on Workers and want the agent's mail to be one more binding next to its Durable Object, R2 bucket and AI model.
  • You need your own domain on the agent's address today. Cloudflare gives it to you on Workers Paid; our custom domains sit behind a plan that cannot be bought yet.
  • Volume matters. Workers Paid includes 3,000 outbound emails a month, then $0.35 per 1,000, and inbound routing carries no charge or volume cap on either plan.
  • You want every line of the mailbox logic in your own repository, including the model that reads mail, the storage schema and the UI.
  • A hard recipient allowlist at the binding, enforced by the platform, is the whole control you need for a narrow notification agent.

Choose Email for Agents if

  • You want a stored, threaded mailbox per agent over REST and remote MCP without writing a MIME parser, a thread table or a reply resolver.
  • A retried send must not become a second email: an Idempotency-Key is honoured, and reusing one with different content is refused.
  • Several agents share a project and each credential has to be limited to named scopes and specific inboxes, not one account token.
  • Outbound mail needs a human decision recorded as an approval resource with an expiry and events, not a confirm button inside one app.
  • You want to rehearse the inbound loop against a test inbox that cannot reach an internet recipient, with a key prefix that cannot touch live.

What Cloudflare actually ships for agent email today

Cloudflare Email Service vs an agent inbox is a build-versus-buy question, and Cloudflare is the most credible “build” answer on the market. Its 16 April 2026 post, titled “Email for agents”, moved Email Sending from private to public beta and folded it together with Email Routing into one product. The docs overview today lists Email Sending as beta on the Workers Paid plan and Email Routing on Free and Paid.

There are four pieces, and it helps to name them precisely because comparison pages keep blurring them:

The first three are platform. The fourth is a sample you deploy and own. That line matters for everything below. If you want the category overview first, start with what email for AI agents means in practice.

The build ledger: what each control costs on Cloudflare

This is the part the other comparisons skip. An agent that reads mail from strangers and can reply needs a fixed set of controls, whoever hosts it. The table maps each one to the Cloudflare primitive that exists today and to what you still write.

ControlCloudflare primitiveWhat you still build
MIME parsingmessage.raw stream; docs recommend postal-mimeAttachment extraction, charset edge cases, size handling
StorageNone in Routing; R2, D1, Durable Object SQLite availableMessage schema, retention, deletion, attachment lifecycle
Threadingmessage.reply() keeps the Message-ID chain, onceA thread model; References and In-Reply-To on every later send
Reply routingcreateSecureReplyEmailResolver with HMAC-signed headersSecret rotation, expiry policy, fallback when a client strips headers
Idempotent sendsNone documentedA send ledger keyed by your own id, plus a decision on timeouts
Per-agent permissionsBinding allowlists; account API tokensPer-agent read, draft and send separation; per-inbox scoping
Human approvalNone in the platformPending state, reviewer UI, expiry, re-check of content at approval time
Audit trailSending metrics and logsWho authorised which send, from which credential, with which payload
Test modeSend and inbound simulation in wrangler devStored fixtures, a CI path that exercises read, reply and thread end to end

None of those rows is exotic engineering. Each one is a week you did not plan for, and a few of them are the rows that cause incidents. The sections below go through the ones where the Cloudflare docs contain a detail that changes the design.

Inbound: a stream, not a mailbox

Email Routing gives your Worker one invocation per message and a raw MIME stream. There is no stored mailbox behind it. If your handler throws, runs out of CPU, or you did not persist the message before returning, the message is not somewhere you can list later. The limits page is explicit that on Workers Free, complex handlers can exceed CPU and memory limits and fail with EXCEEDED_CPU, and it recommends Workers Paid for that reason. So the realistic inbound design is: parse with postal-mime, write the parsed record and attachments to storage inside the handler, then hand off. Agentic Inbox does exactly this, one Durable Object with SQLite per mailbox and R2 for attachments, which is a good design and also code you now maintain.

Replying inside the handler has sharp rules, and they are worth knowing before you build around reply(). The email handler docs state that reply() throws unless the incoming email has a valid DMARC result, the reply goes to the original sender, the outgoing sender domain matches the domain that received the mail, and it is the only reply for that event. It also throws if the incoming message carries more than 100 References entries, a deliberate loop brake. That means the one-shot synchronous reply is unavailable for any sender with a failing or missing DMARC result, and any asynchronous reply, the kind an agent sends an hour later after checking three systems, goes out through send_email instead. On that path, threading is your job. The Agents SDK’s sendEmail() takes an inReplyTo option that sets In-Reply-To, but keeping the Message-ID you are answering, and building References, comes from your own storage. The failure mode is quiet, because a broken References chain forks the conversation in the recipient’s client while your send succeeds. Threading for agents covers why this breaks in practice.

The Agents SDK closes part of this gap. sendEmail() injects X-Agent-Name and X-Agent-ID headers and, given a secret, signs them with HMAC-SHA256 and a timestamp; createSecureReplyEmailResolver verifies the signature and expiry before routing the reply back to the same agent instance. That is a clean answer to “which agent owns this reply”. It is not a stored thread, and it depends on the reply carrying the signed headers back.

Here the inbox is the stored object. Threads and messages are REST resources under /projects/{project_id}/inboxes/{inbox_id}/threads, and the same data sits behind the list_threads, list_messages and get_message MCP tools. Nothing needs to be caught in a handler to exist later.

Outbound: retries, and the missing idempotency key

This is the most concrete gap for an agent workload. Neither the Workers API page nor the REST API page documents an idempotency key. The binding returns a messageId on success and throws an Error with a code on failure, including E_RATE_LIMIT_EXCEEDED, E_DAILY_LIMIT_EXCEEDED, E_DELIVERY_FAILED and E_INTERNAL_SERVER_ERROR. The REST response sorts recipients into delivered, permanent_bounces and queued.

Now put an agent in front of it. A tool call times out after the request left your Worker. The model, reasonably, calls the tool again. Without a key the platform can dedupe on, the only protection is a ledger you write: record an intent keyed by your own id before calling send(), and decide what “timed out, outcome unknown” means before retrying. Skip that and the recipient gets the email twice. The full failure shape is in why an agent sends the same email twice.

On this side a send accepts an Idempotency-Key. The key and a SHA-256 of the payload are stored together, so a retry with the same content inside the 30-day replay window returns the original result, and a retry with different content under the same key fails with idempotency_conflict instead of sending something new (packages/domain/src/services.ts, around line 1616). The harder case stays honest: if the provider’s answer is lost, the message is recorded as message.submission_unknown and it does not resolve itself later in this build.

Cloudflare is better on sending economics, and it is not close. Workers Paid includes 3,000 outbound emails a month per account, then $0.35 per 1,000. Emails rejected at the API boundary, including suppressed recipients, do not count. Inbound carries no charge or volume cap on either plan. New accounts start on a conservative daily quota that rises with sending reputation, recipients are capped at 50 per message, and total message size is 5 MiB, or 25 MiB to verified destination addresses.

Permissions: account tokens and binding allowlists

Cloudflare’s sending controls live in two places. The REST endpoint authenticates with an account API token that has permission to send emails, and the sender domain must be onboarded on the account that owns the token. A send_email binding can be restricted with destination_address, allowed_destination_addresses or allowed_sender_addresses; a send outside the list fails with E_RECIPIENT_NOT_ALLOWED. That allowlist is a real, platform-enforced control, and for a notification agent that only ever emails ops, it is the whole security model you need. Credit where due: we do not ship a one-for-one equivalent of a fixed recipient allowlist on the binding.

What the platform does not describe is separating reading from drafting from sending for a given agent, because reading is your code touching your storage. Agentic Inbox shows where that leaves a real app. Its README says any user who passes the shared Cloudflare Access policy can access all mailboxes, including through the MCP server at /mcp, where an external tool “can operate on any mailbox by passing a mailboxId parameter”, and that “there is no per-mailbox authorization; the Cloudflare Access policy is the single trust boundary.” That is a sensible choice for a single-owner inbox and a stated one. It is also the boundary you must redesign the moment two agents with different jobs share a deployment.

Here that boundary is the product. There are 32 named scopes, messages:read and drafts:write separable from messages:send, and credentials pinned to specific inbox ids. The MCP server filters tools/list by the connection grant and re-checks on tools/call, so a read-and-draft grant yields a session where send_message does not exist. An API key is rejected at https://mcp.emailforagents.ai/mcp by design; only an OAuth connection grant may call tools. The model is laid out on the developers page, and the reasoning in agent email permissions and scopes.

Approval and audit: a confirm button versus a record

Agentic Inbox’s README says its agent auto-drafts replies to inbound mail, “always requiring explicit confirmation before sending”. That is the right instinct and it is a UI behaviour of one app. The Email Service docs describe no pending-approval state, no expiry and no event when a reviewer says no, so a second agent written against the raw binding gets none of it.

Building a real approval step on Cloudflare means a pending record, a reviewer screen, an expiry, and one easy-to-miss rule: the approved content must be the content that leaves. If the agent edits a draft after a human read it, the approval should not carry over. Here a held send writes an approval in state pending that expires after seven days (services.ts:1662), emits approval.requested, and emits approval.rejected when a reviewer refuses. Approving a draft whose content no longer matches the content the approval was raised for is refused with version_conflict (services.ts:2051). Two further approval events are declared in the contract but not emitted in this build, so do not build on them. The runtime side is on the controls page, and the design is in human approval for agent email.

Audit follows the same pattern. Cloudflare gives you sending metrics and logs, and notes that sends from a Worker show as “dropped” in the Email Routing summary even when delivered, so outbound tracking belongs in the Email Sending observability view. Who authorised a given send, and with which credential, is a table you design. Here the console shows every thread and human control changes are written to an audit log with the actor and a before and after diff. Audit logs are excluded from project exports, which is a gap if you need them in your own warehouse.

Testing without sending

Cloudflare’s local story is good, and it covers both directions. By default wrangler dev simulates the send binding and logs sends to the console, remote bindings let a local Worker send real mail through the service, and a raw RFC 5322 message POSTed to /cdn-cgi/local/email invokes your local email() handler as if it had arrived. What that exercises is the handler. Whether the agent can later list the message, reply to it and keep the thread intact depends on the storage and threading code you wrote, so a CI test of the full loop is a harness you build around it.

Here test inboxes use a non-routable .mail.invalid address and a simulated transport, up to three active per workspace, the last 100 messages retained, content expiring after seven days, and /projects/{project_id}/test/inbound injects mail into the agent’s own inbox. An efa_test_ key resolves only to test projects, and no flag lets it reach live. The walkthrough is in testing agent email without sending.

Cloudflare Email Service vs agent inbox: which to pick

The recommendation flips on one question: is the agent’s email a feature of a Workers app you already own, or a separate identity several agents and humans need to share safely?

If it is a feature, build on Cloudflare. You get your own domain today with SPF, DKIM and DMARC set up on onboarding, sending that costs a fraction of a cent per message, free inbound with no volume cap, and the agent’s state, storage and model on one platform. The constraints are real but acceptable: the domain must use Cloudflare DNS, Email Sending is still beta, and every row of the build ledger above lands in your backlog. If a binding-level recipient allowlist covers your risk, you may never need the rest.

If it is an identity, the ledger is the product you would be building. That is what we ship, at the cost of quota, price and custom domains, all of which Cloudflare wins today. The comparisons hub lines this up against the other options.

Sources

Everything stated above about Cloudflare Email Service was read from these pages on . Products change; if a claim here is stale, it is a bug worth reporting.