Connect Cloudflare

Send transactional email through Cloudflare Email Service and receive it with Email Routing, in your own Cloudflare account. Two API tokens, what Stamp0 deploys, limits, costs and the transactional-only rule.

A Cloudflare connection is one Cloudflare account. Like an AWS connection, it belongs to your organization and any project can use it, and you choose per domain which connection sends and which receives: acme.com can send through Cloudflare while news.acme.com sends through SES.

Beta

The Cloudflare provider is in beta for every workspace, because Cloudflare Email Service sending is in public beta at Cloudflare (Email Routing, which receives, is generally available). Cloudflare can change the sending API, its limits and its pricing before general availability. The limits on this page still apply: transactional mail only, at most 2 emails per second per connection, and a daily quota that starts around 1,000 emails on a new Cloudflare account.

Before you start

  • Workers Paid. Cloudflare Email Service sending needs the Workers Paid plan ($5 a month), and Stamp0 requires it for every Cloudflare connection, including receive-only ones. Verification fails with cloudflare_plan_required without it.
  • Cloudflare DNS. Each domain's zone must use Cloudflare DNS, for sending and for receiving. A domain whose DNS is elsewhere answers 422 zone_not_on_cloudflare.
  • Transactional email only. See Transactional only before you connect: Cloudflare can disable sending for your whole account if it sees bulk mail.
  • An owner or admin. Connections are created and changed in the dashboard only. API keys, the CLI and MCP can read a connection's status but never hold a Cloudflare token.

Two tokens

The token Stamp0 keeps cannot deploy code. Changes to what runs in your account use a second token that you supply each time and that Stamp0 never keeps.

The setup token (never kept)

Used to deploy and change Stamp0's resources in your account: the first deploy, upgrades, rollbacks after the watch window, redeploys after a change outside Stamp0, adding or removing a sending domain, turning receiving on or off for a name, key rotation, and removal. Stamp0 holds it sealed for at most one hour while a deploy runs as a background job (never in the job itself), then deletes it; for a domain change it is used for that one request only. Give it a short expiry when you create it.

PermissionScopeWhy
Workers Scripts WriteAccountDeploy, upgrade and roll back the Stamp0 Worker; event subscriptions
Workers R2 Storage WriteAccountCreate the mail bucket and its retention rule
Queues WriteAccountCreate the queue, its dead-letter queue and event subscriptions
Email Sending WriteAccountOnboard and remove sending domains, turn email preview off. Add it by hand (below)
Zone Settings WriteThe zones you send or receive onTurn Email Routing on and off for a receiving name, and set subaddressing
Email Routing Rules WriteThe zones you receive onPoint the apex catch-all at the Worker (apex_catch_all), and remove Stamp0's rules when receiving is turned off or the connection is removed. Add it by hand
DNS WriteThe zones you send or receive on, and the zones of Amazon SES domains you publish on CloudflareRead your apex MX, SPF and DMARC before and after enabling Routing, delete the apex SPF Cloudflare adds when you had none, and roll back any other change. Publish an Amazon SES domain's records when you ask (below). When you delete a domain, remove the records Stamp0 published and the DMARC p=reject Cloudflare created at onboarding, each only while unchanged
Zone ReadThe zones you send or receive on, and those SES zonesFind zones and read Email Routing status
Account Analytics ReadAccountRead the Stamp0 Worker's error rate during the 30-minute watch window after a deploy, when the runtime token cannot (Versions and upgrades)

Stamp0 never creates API tokens, so it does not need Account API Tokens Write, and it never uses Email Routing destination addresses (rules go to the Worker), so it does not need Email Routing Addresses. When Cloudflare refuses a setup-token call, the API answers 422 validation_error with setup_token_permissions: and the permission the call needed.

The runtime token (stored, sealed)

Used for everything day to day: sending, pulling events and inbound pointers from the queue, reading stored mail, inbox rules and verification.

PermissionScopeWhy
Email Sending WriteAccountSend, keep Cloudflare's suppression list in step, read the daily quota and reputation. Add it by hand (below)
Email Routing Rules WriteThe zones you chooseAdd and remove a rule per inbox. Add it by hand
Zone ReadThe zones you chooseFind zones
Zone Settings ReadThe zones you receive onRead whether Email Routing is ready on each receiving name, and its records (Zone Read alone cannot). Runtime tokens created before October 20, 2026 lack it: add it (below), or receiving names stay setup_token_required
Queues WriteAccountPull and acknowledge messages from Stamp0's queue (Cloudflare has no pull-only permission)
Workers R2 Storage Bucket Item WriteThe stamp0-<id> bucket onlyRead and write stored mail. Add it by hand (below)
Workers Scripts ReadAccountCheck the deployed Worker version for drift; it cannot deploy
Account Analytics ReadAccount, optionalRead what the whole Cloudflare account sent today against the daily quota, mail sent outside Stamp0 included (Sending), and the Worker's error rate after a deploy

When you verify, Stamp0 checks every permission with a read call and shows a named checklist, and checks that the runtime token cannot upload a Worker: a token that could is refused. Replacing the token (Replace runtime token) runs the same checks before the swap. If Cloudflare rejects it later, the connection shows credential_invalid.

When a re-check finds a permission missing (other than Zone Settings Read alone, below), the connection goes to error with last_error permission_missing: and the checks that failed (permission_missing:queues,r2_bucket), and the dashboard names them.

Adding Zone Settings Read to an older token

A runtime token created before October 20, 2026 has no Zone Settings Read. It keeps sending: verification leaves the connection connected and adds the notice runtime_token_permission_missing with detail zone_settings_read, but receiving names stay setup_token_required because Stamp0 cannot read Email Routing's status. To fix it:

  1. In the Cloudflare dashboard, open My Profile, then API Tokens, and Edit the Stamp0 runtime token.
  2. Add Zone, Zone Settings, Read, and include the zones you receive on.
  3. Save. Cloudflare keeps the token's value, so there is nothing to replace in Stamp0.
  4. Re-verify the connection (or wait for the next re-check). The notice goes away.

The dashboard's Create the token in Cloudflare links open Cloudflare's token form with the token named and every permission Cloudflare has a template key for already selected. Cloudflare publishes no key for Email Sending or Email Routing Rules (and refuses a link that asks for one it does not recognize), so next to each link the dashboard shows Add these in Cloudflare before creating the token, with the exact rows to add:

TokenPermissionResourceAccessScope
SetupEmail Sending WriteAccountEditYour account
SetupEmail Routing Rules WriteZoneEditThe zones you receive on
RuntimeEmail Sending WriteAccountEditYour account
RuntimeEmail Routing Rules WriteZoneEditThe zones you receive on
RuntimeWorkers R2 Storage Bucket Item WriteAccountEditThe stamp0-<id> bucket only

When the token is for a domain you are adding, the dashboard names that domain's zone. A template link also cannot pick zones or buckets, so set the zone and bucket scope of every row in the form, as the tables above say.

R2 is added by hand on purpose. Cloudflare's template has only one R2 key, and it pre-selects Workers R2 Storage Write on the whole account, which would let the stored token read and write every bucket you have; a template also cannot pick a bucket. So the runtime link leaves R2 out: in Cloudflare's token form, add Workers R2 Storage Bucket Item Write yourself and choose the stamp0-<id> bucket the dialog names. For the same reason Stamp0 never hands out presigned R2 links (Cloudflare mints their credentials only for account-wide R2 tokens): attachment downloads go through Stamp0 (see Sending).

Connecting

  1. In the dashboard, choose Cloudflare, enter your account ID, and open the setup-token link. Cloudflare's token page opens with the permissions pre-selected where Cloudflare has a template key. Add the ones the dialog lists under Add these in Cloudflare before creating the token (above), set each row's zones, and compare with the full list the dialog shows.
  2. Tick the acknowledgment: Cloudflare Email Service is transactional only. If Cloudflare sees bulk mail it can disable sending for the whole account, including mail you send outside Stamp0. It is recorded in your audit log.
  3. Paste the setup token. Stamp0 deploys its data plane (below); then you create the runtime token from a second link that names the bucket, with exactly the bucket scope.
  4. Verification runs and shows the checklist. The connection is then connected.

What Stamp0 deploys in your account

Every name ends in a short id derived from your connection (s0…), and every id is recorded on the connection. Changes and removal only ever touch what is recorded there.

ResourceNameNotes
R2 bucketstamp0-<id>Inbound mail and outbound content. Deleted by age after the retention period (30 days by default, never under 21). Kept when you remove the connection
Queuestamp0-<id>Inbound pointers and sending events. 14-day retention, up to 100 delivery attempts
Dead-letter queuestamp0-<id>-dlqDrained automatically every few minutes
Workerstamp0-<id>Receives mail from Email Routing and stores it (below). No npm dependencies, no MIME parsing
Event subscriptionsone per sending domainDelivery events to the queue; created before the domain can send
Email Routing rulesstamp0:<id>:<inbox>One per inbox address. Stamp0 never edits or deletes a rule without its own prefix
Catch-all rulestamp0:<id>:catch_allOnly with apex_catch_all; the rule it replaced is recorded and put back

Versions and upgrades

The Worker is built reproducibly and signed; Stamp0 refuses to deploy a bundle whose hash or signature does not match the release. Stamp0 never upgrades it on its own: the connection shows data_plane.status (up_to_date, update_available, update_required for security fixes and major versions, or modified when the Worker was changed outside Stamp0), and an owner or admin applies the change with a setup token. After that, Roll back is one click with a setup token.

The watch window. For 30 minutes after an upgrade or redeploy, while the setup token is still held, Stamp0 checks the new version every minute and rolls back to the previous one automatically when:

  • more than 1% of the Worker's invocations throw (from at least 20), read from Cloudflare's Workers analytics with the runtime token, or with the setup token when the runtime token has no Account Analytics Read; with neither, only the next check applies;
  • any message the new version stores fails Stamp0's signature check.

The rollback shows on the connection (data_plane_rolled_back_exception_rate or data_plane_rolled_back_bad_pointer). A redeploy right after someone edited the Worker in Cloudflare's dashboard keeps the last version Stamp0 deployed as the one to roll back to, never your edit. The first deploy has nothing to roll back to, so it has no watch window.

Pinning. An owner or admin can keep a connection on one version: PUT /connections/:id/data-plane/pin with { "version": "1.0.1" } (or no body for the deployed version), or { "version": "opt_out" } to refuse every upgrade. A pinned connection answers 409 conflict to an upgrade, redeploy or key rotation onto any other version (with opt_out, to every one) before anything changes; rollback still works. DELETE /connections/:id/data-plane/pin removes the pin. The connection shows it as data_plane.pinned and data_plane.pin, and both changes are in the audit log. Pinning deploys nothing. A version superseded by a security fix cannot be pinned.

Deploy pause (kill switch). Stamp0 can pause every data-plane deploy and upgrade across all connections, for example during an incident. While deploys are paused, new Cloudflare connections answer provider_not_configured, and POST /connections/:id/data-plane answers 409 conflict for everything except rollback, which restores a version you already approved. Sending, receiving and pulls keep running.

Removing the connection

With a setup token, Stamp0 deletes its Worker, rules, event subscriptions and queues, puts back an apex catch-all and subaddressing setting it changed, and turns Email Routing off on a name only if Stamp0 turned it on and no other rule uses it. The R2 bucket is kept (it holds your mail) and the dashboard says so. Without a setup token, the connection is removed from Stamp0 and you get the exact list to delete by hand. While domains still use it, the connection stays disconnected and its data_plane.status is destroyed, with no version, since Stamp0 no longer runs anything in the account for it.

Domains

Cloudflare creates and locks the DNS records for a Cloudflare domain; Stamp0 writes none of them and you should not edit them. Enable on Cloudflare (with a setup token) onboards the domain:

  • DMARC. Cloudflare never overwrites an existing _dmarc record. When there is none, it creates v=DMARC1; p=reject;, which rejects any of your other mail for that domain that is not aligned. Stamp0 needs your confirmation first (confirm_dmarc_create): without it, POST /domains answers 422 validation_error with a message starting dmarc_confirmation_required: and planned_records, every record onboarding would create, so you can review them; the dashboard lists them in the dialog. On an apex, create a p=none record yourself first unless you are sure. The domain's DMARC record shows the policy actually published (yours, or Cloudflare's), read by DNS.
  • Email preview. Cloudflare keeps the full content of every message (password reset links and one-time codes included) for about 7 days, visible to anyone with access to your Cloudflare dashboard. Stamp0 turns it off unless you tick Keep Cloudflare email preview (keep_email_preview), and the DNS advisor warns if it is switched back on.
  • Suppressed recipients are dropped from a send instead of failing the whole request.
  • Events first. A domain stays pending until its event subscription exists, so no delivery event is lost.
  • One subscription per domain. Cloudflare allows one Email Service event subscription per sending domain. If the domain already has one that Stamp0 did not create (from another tool, or your own Worker), onboarding answers 409 provider_subscription_conflict naming it, before anything is changed; Stamp0 never takes it over. Delete it in your Cloudflare account or use another domain. If a later onboarding step fails, Stamp0 undoes what it changed (the sending subdomain it created, or the email-preview and suppressed-recipient settings of one that existed).

Onboarding runs with a setup token you pass for that request only (setup_token on POST /domains); removing a Cloudflare domain (DELETE /domains/:id with { "setup_token": … }) deletes its sending subdomain and event subscription, and its Email Routing rules if it receives on Cloudflare, the same way. A domain whose Cloudflare connection was removed (destroyed or disconnected) still has its sending subdomain, and possibly Email Routing, in your account: deleting it answers 409 connection_not_ready unless you pass a setup_token (Stamp0 removes what it set up for the name) or force: true (the domain is deleted and the response's left_behind lists what to remove by hand; the connection shows the same list as a notice). A domain added for receiving only (capabilities.sending: "disabled" and no sending connection) gets no sending identity on any connection. Open and click tracking, a custom tracking domain and enforced TLS are not available on Cloudflare and are refused.

A domain is verified when Cloudflare reports its records ready and the records resolve.

Amazon SES domains on Cloudflare DNS

A domain that sends through Amazon SES but whose DNS is on Cloudflare can have its records published for you through a Cloudflare connection of your organization: Publish on Cloudflare on the domain page, or

POST /domains/:id/dns/cloudflare
{ "connection_id": "con_cloudflare…", "setup_token": "…" }

The setup token needs Zone Read and DNS Write on that zone and is used for this request only; the stored runtime token never gets DNS Write. Stamp0 adds the DKIM CNAMEs, the MAIL FROM MX and SPF, and a v=DMARC1; p=none; record only when the domain has no DMARC policy (yours stays). Each record it creates carries the comment managed by stamp0 (<domain id>). It never overwrites: a name that already holds a different value is reported as conflict and left alone, and a receiving MX is always yours to add (manual), since it decides where the name's mail goes. The answer is the domain, whose dns_automation (provider: "cloudflare", applied or partial) lists the result per record; verification then starts again. A domain that sends on Cloudflare needs none of this: Cloudflare publishes its records itself.

Deleting the domain removes the records Stamp0 created, each by its id and only while it still holds what Stamp0 wrote (a record you changed since is yours and stays), so the delete needs a setup token with DNS Write on the zone. Without one it answers 422 setup_token_required, unless you pass force: true, which deletes the domain and lists the records in left_behind. The same goes for the _dmarc p=reject record Cloudflare creates when a domain onboards without a DMARC record.

Receiving

Each domain receives on one connection, chosen independently of the one that sends: in.acme.com can receive on Cloudflare while acme.com sends through SES. A domain that sends on Cloudflare receives on SES or on a Cloudflare connection of the same Cloudflare account: one setup token covers one account, so another account's connection is refused (422 validation_error). Receiving on Cloudflare uses Email Routing in your account and the Stamp0 Worker; nothing passes through Stamp0's servers until Stamp0 reads the stored message from your bucket.

POST /domains
{
  "name": "in.acme.com",
  "sending_connection_id": "con_ses…",
  "receiving_connection_id": "con_cloudflare…",
  "receiving": { "enabled": true, "mode": "subdomain_rules" },
  "setup_token": "…"
}

PATCH /domains/:id turns receiving on ({ "receiving": { "enabled": true }, "setup_token": … }), off ({ "receiving": { "enabled": false }, "setup_token": … }), or moves it to another connection (receiving_connection_id). Moving between two Cloudflare connections is two steps: off, then on with the other one. The response's receiving_connection names the connection and its provider, and capabilities.receiving on the connection says whether it can receive. In the dashboard, Receive through in the add-domain dialog does the same, and the domain's Receiving switch asks for a setup token on Cloudflare. The CLI takes the token on stdin: printf %s "$TOKEN" | stamp0 domains update dom_… --receiving on --setup-token-stdin. The MCP server never takes a setup token, so it refuses to add a domain that sends or receives on Cloudflare and points to the dashboard or the CLI; once the domain exists, the MCP tools follow it.

Turning Email Routing on, safely

Stamp0 turns Email Routing on for the receiving name only (never the whole zone), and guards your other mail:

  1. It reads your apex MX, apex TXT (SPF) and _dmarc records, checks the setup token can delete records (DNS Write), then turns Routing on for the name.
  2. It reads them again. Cloudflare adds its routing DKIM key (cf2024-1._domainkey) the first time, and an apex v=spf1 include:_spf.mx.cloudflare.net ~all when your apex had no SPF: Stamp0 deletes that SPF record at once (the name keeps receiving without it). Any other change is rolled back: Routing goes off on the name, what it added is deleted, and the API answers 422 unsupported_by_provider with mx_conflict: and the change.
  3. It turns on subaddressing for the zone (so support+orders@ reaches support@), recording the previous value, which is put back when the connection is removed.

A name on a zone whose apex receives through another provider (Google Workspace, Microsoft 365) works as a subdomain: the apex is untouched.

Modes

receiving.modeWhereWhat receives
subdomain_rules (default)A subdomain (in.acme.com)Only addresses with an inbox. Each inbox address gets its own Email Routing rule, stamp0:<id>:<inbox id>, to the Stamp0 Worker, added and removed with the inbox (runtime token). Mail to any other address is refused by Cloudflare (550 5.1.1); a new inbox is activating for its first five minutes (see below). Cloudflare allows 200 rules per zone; the 201st inbox answers 422 unsupported_by_provider (rule_limit_reached:) and is not created
apex_catch_allThe zone apex (acme.com)Every address. Needs confirm_mx_takeover: true, and is refused (mx_conflict:) when another provider's MX is published on the apex. The zone's catch-all rule is pointed at the Worker; the rule it replaced is recorded and put back when receiving is turned off or the connection is removed

subdomain_rules on the apex is refused (mx_conflict:), so receiving never silently takes over a domain's mail. The mode is chosen when receiving is turned on: a receiving.mode on a domain that already receives answers 422 validation_error; turn receiving off, then on again with the new mode. A domain whose DNS is not an active zone of this Cloudflare account answers 422 zone_not_on_cloudflare, with a message that starts zone_not_on_cloudflare:.

How a message arrives

  1. Cloudflare checks SPF, DKIM and DMARC and refuses mail that fails both SPF and DKIM, or an enforcing DMARC policy. It runs the Worker once per recipient.
  2. The Worker stores the raw message in your R2 bucket with a signed pointer in the object's metadata, then puts the pointer on Stamp0's queue and rings Stamp0's doorbell.
  3. Stamp0 pulls the queue (on the doorbell, and every minute), verifies the pointer's signature, checks the stored bytes against it, and only then reads the recipients and SPF, DKIM and DMARC verdicts from the pointer: never from headers in the message.
  4. Copies of one message to several inboxes become one received email (same Message-ID and the same content after Cloudflare's own trace headers) listing every inbox, with one receipt per copy. A redelivered pointer changes nothing.
  5. Pointers that fail repeatedly reach the dead-letter queue, which Stamp0 drains every five minutes; an hourly sweep reads the signed pointers of stored objects back to the retention horizon and recovers any message whose queue entry was lost. Objects without a valid signature are never processed.

A new inbox needs a few minutes. Cloudflare takes one to three minutes to start accepting mail for a new address. A new Cloudflare inbox shows activating for its first five minutes; mail sent to it meanwhile may be refused with 550 5.1.1 and should be sent again. Stamp0 never suppresses your own inbox addresses because of such a refusal.

Received mail then appears in GET /emails/receiving, the domain's inboxes and threads, and email.received webhooks, as for SES. Downloads (raw.download_url, attachments) are links to the Stamp0 API (GET /emails/receiving/:id/raw, GET /emails/receiving/:id/attachments/:attachmentId/content), which reads the object from your bucket with the runtime token: they need the same API key (or dashboard session) as the read that returned them and expire after an hour.

Status

receiving.status is enabled once Cloudflare reports the name ready. While it is pending, receiving.setup.status says why: setup_token_required (Routing is off for the name, for example turned off in Cloudflare: turn it on again with a setup token), routing_misconfigured (Cloudflare reports a missing or unlocked record), data_plane_missing (the Worker is not deployed). Every verification pass reads Routing's status again, so a name whose Routing was turned off (or a record unlocked) in Cloudflare goes back to pending with the reason; Check DNS now (POST /domains/:id/verify) runs a pass at once, and also puts back the rule of any inbox that lost it.

Turning it off

With a setup token, Stamp0 deletes its own rules for the name, restores the catch-all it replaced, and turns Routing off for the name only if Stamp0 turned it on and no rule of yours remains there. Deleting the domain does the same first.

Sending

  • Throughput. Stamp0 sends through Cloudflare's REST API at 2 emails per second per connection at most (about 170,000 a day). Sends past that are queued, never refused.

  • Daily quota. Cloudflare also has a daily quota that grows with your reputation; new accounts start around 1,000 a day. It belongs to the Cloudflare account, so every connection on the account, and mail you send outside Stamp0, uses the same quota. The connection shows what the account sent in the current window (sending_quota.sent_last_24_hours) and where the figure comes from (sent_source):

    • cloudflare_analytics: Cloudflare's email analytics, every send on the account (needs Account Analytics Read on the runtime token; the figure is never below Stamp0's own count, since analytics lag a few minutes);
    • stamp0_records: Stamp0's own count across every connection on the account, when the analytics cannot be read.

    The window resets when Cloudflare says (resets_at, which need not be midnight; the next UTC midnight when Cloudflare gives no time). Cloudflare's own usage.sent counter is not used: it does not keep up. Sends past the quota wait in the queue as quota_exceeded for the next window, and so does everything after Cloudflare itself reports the quota spent.

  • Attachments. GET /emails/:id/attachments links each attachment to GET /emails/:id/attachments/:attachmentId/content on the Stamp0 API, which reads it from your bucket with the runtime token and answers it as a download. The link needs the same API key (or dashboard session) as the listing and expires after an hour.

  • Message-ID. Cloudflare sets it and it cannot be overridden; Stamp0 stores it and adds X-Stamp0-Email-Id. Replies keep In-Reply-To and References.

  • Bounces and complaints on any connection block the address for the whole organization, on SES as well. When the recipient's server reports a hard bounce or a complaint on a Cloudflare connection, Stamp0 also adds the address to Cloudflare's suppression list for each of this connection's sending domains (by the domain's own name, never account-wide), retrying for about a day if Cloudflare is busy. Stamp0's list stays the record: removing the address in Stamp0 stops a write that has not happened yet.

  • From must be a domain this connection onboarded.

Transactional only

Cloudflare's documentation says Email Service "supports transactional email only", and its enforcement (bounce, complaint and spam-rejection rates; repeated bulk mail) can disable sending for your whole Cloudflare account, including mail you send outside Stamp0. Stamp0 keeps you on the safe side:

  • Campaigns, /broadcasts, sequences and preference-center broadcasts on a Cloudflare domain answer 422 unsupported_by_provider. Campaigns and sequences already using a domain that moves to Cloudflare are paused.
  • A batch in which more than 50 messages share a subject answers 422 unsupported_by_provider.
  • List-Unsubscribe is accepted (Cloudflare signs it), but a batch send that carries it puts a bulk_headers_on_cloudflare notice on the connection for a week.
  • One confirmation email per signup (double opt-in) is allowed.

The provider terms say the same.

Limits compared with SES

Amazon SESCloudflare
Campaigns, sequences, broadcastsYesNo (422)
Open and click trackingYesNo (422)
Message size40 MiB5 MiB (422)
Recipients per message5050
Custom MAIL FROM, Message-ID, Date, Return-PathPartlyNo (422)
Custom headersAllowedX-*, List-*, In-Reply-To, References, Precedence, Auto-Submitted, Importance, Priority and similar; up to 20 non-X- headers, 16 KB in all
Inline images (content_id)YesYes
Send rateYour SES quota2 per second per connection
Inbound size40 MiB25 MiB
Receiving addressesThe whole domainOne rule per inbox (200 per zone), or the apex catch-all
Region and sandboxYesNone
DNS elsewhere than the providerYesNo (zone_not_on_cloudflare)

Reputation thresholds

Deliverability judges Cloudflare domains against Cloudflare's own policy: bounces warn at 2% and put the account at risk at 5%, complaints at 0.1% and 0.2%, each from 100 sends. Cloudflare suspends sending after a week at risk. A project that sends through SES and Cloudflare is judged against the stricter of both (health.basis: "strictest").

What it costs

You pay Cloudflare directly, on your own bill (list prices, October 2026):

ItemPrice
Workers Paid$5 a month
Sending3,000 emails a month included, then $0.35 per 1,000 (each recipient counts)
Receiving (Email Routing)Free
R2 storage10 GB-month free, then $0.015 per GB-month
Queue operations1 million free, then $0.40 per million (each message is written, read and acknowledged)

Plans

Provider connections are counted per provider: Free and Pro include one Amazon SES connection and one Cloudflare connection; Scale has no limit. See pricing.

On this page