Skip to content

Choose a sending transport: Cloudflare, SES, Resend or SMTP

Four transports with genuinely different ceilings and genuinely different event data. The real limits, straight out of the adapters, plus what failover does and does not buy you.

DeployIntermediate12 min readUpdated

01The limits, as the code enforces them

These are not marketing numbers. They are the *_LIMITS constants the send path enforces, imported into this page, so what you read here is what your instance will refuse.

TL;DR

Fifty recipients everywhere; the message ceiling is the number that differs, and it is measured on the rendered message rather than on your attachment.

TRANSPORT COMPARISON

Ceilings read from CLOUDFLARE_LIMITS, SES_LIMITS, RESEND_LIMITS and SMTP_LIMITS in @mailysend/providers.

TransportMessageAttachmentRecipients
Cloudflare Email Service5 MiB5 MiB50
Amazon SES40 MiB40 MiB50
Resend40 MiB40 MiB50
Raw SMTP relay25 MiB25 MiB50
Cloudflare Email Service
Events
Yes — a Queues subscription per sending domain.
Best for
A Cloudflare deployment that wants no third-party account at all.
Watch out
Public beta, so pricing and limits can change. The smallest message ceiling of the four.
Amazon SES
Events
Only with an SNS configuration set. Without one there are no events.
Best for
Large volume, large attachments, and the lowest per-message price.
Watch out
Sandbox limits until you request production access, and events are opt-in.
Resend
Events
Yes.
Best for
Keeping an existing Resend account while moving everything else in-house.
Watch out
You are still paying a per-message vendor, which may be the point or may not.
Raw SMTP relay
Events
No. A 250 is all you get; bounces arrive later as DSNs.
Best for
An existing relay you already operate, or a provider with no HTTP API.
Watch out
No delivery events means no open, click or bounce data from the transport itself.
The four transports, side by side
Cloudflare
SES
Resend
Raw SMTP
Message ceiling
5 MiB
40 MB
40 MB
25 MiB
Header budget
16 KiB
100 KiB
100 KiB
100 KiB
Recipients per send
50
50
50
50
Subject characters
998
998
998
998
Measured before it leaves
Yes
Yes
No
Yes
Delivery events
Yes
With an SNS configuration set
Yes
No
Published daily quota
No
No
No
No

The ceiling is measured on the rendered message. Not on your attachment, and not on your HTML. Three of the four adapters build the complete RFC 5322 message, sign it, and measure the bytes before anything touches the network. Attachments are base64 in a MIME part, which costs about a third on top of their raw size, and text parts are quoted-printable, which costs a little more again on non-ASCII content. A 4 MB PDF is comfortably over Cloudflare Email Service’s 5 MiB ceiling by the time it is a message. If you are anywhere near a limit, the number to compare against is roughly four-thirds of your attachment bytes, plus the body.

Resend is the exception, and it is worth knowing. The Resend adapter posts structured JSON rather than raw MIME, so it never renders the message locally and never measures it. Its 40 MB entry is what Resend accepts, not something checked before the request goes out; an oversized message comes back as an HTTP error and is classified from the status code — anything that is not 429, 401, 403 or a 5xx becomes permanent. Same outcome, one network round trip later, and the error text is Resend’s rather than ours.

Recipients are 50 everywhere, deliberately. That is 50 across to, cc and bcc combined, counted before the send. Most relays would accept far more, but a large RCPT list is a spam signal at many receivers and the API contract promises the same number on every transport — consistency is worth more here than squeezing a relay. The header budget is the one place the four diverge quietly, and it only matters if you are stuffing metadata into custom headers.

NOBODY PUBLISHES A DAILY QUOTA
All four transports carry dailyQuota: null, because none of them publishes a number worth hard-coding. Cloudflare’s ramps with reputation and is not documented; the sending-domain actor learns the real ceiling from rejections instead of inventing one. If you need a guaranteed rate on day one, that is a conversation with SES about production access, not a setting here.

02Which transports report delivery events

The size ceilings are the difference people notice. Event reporting is the difference that costs them a week, three months in, when someone asks why the bounce chart is empty.

TL;DR

Two of the four report events with nothing to configure, one reports only if you configured it, and one cannot report at all.

TransportWhat comes backWhat you have to do
Cloudflare Email ServiceDelivery events by default, over a Queues subscription scoped per sending domain.Nothing.
ResendDelivery events by default, over its webhooks.Nothing.
Amazon SESDelivery, bounce and complaint notifications — but only with an SNS configuration set. Without one, SES accepts your mail and tells you nothing.Set configuration_set in the provider form, with a subscription pointed back at this instance.
Raw SMTP relayNothing. A 250 says the relay accepted responsibility for the message, and the protocol offers no further callback.Wire up DSNs to your return path, and publish its MX record.

A missing SNS configuration set is the single most common reason a migration to SES appears to “lose” deliverability data.

What a DSN gets you, and what it does not. A DSN is a multipart/report; report-type=delivery-status message delivered to your return path, and its machine-readable part carries the original recipient, an action, a status code and a diagnostic string. That is enough to classify a bounce properly and suppress the address. It is not enough to tell you a message was delivered, because a successful delivery generates no report at all — which is why sent is the last thing an SMTP transport can say for certain, and why the return-path MX record stops being optional on this transport. No MX, no DSN, no bounce data of any kind, and only some DSNs arrive at all.

Opens and clicks are the exception. MailySend tracks those itself either way, since they are your own pixel and your own redirect, so those two charts look the same on all four transports and the delivery-side charts do not. But what an open actually means is its own conversation.

ONE THING SURVIVES EVERY TRANSPORT: YOUR OWN ID
The email id is minted before any provider is called and is stamped into the message as both Message-ID and an X-MailySend-Id header. The provider’s own id is recorded next to it for correlation, never as the identity. That is what lets a DSN arriving three days later — through a transport you have since stopped using — still be matched back to the send it belongs to.

03Picking a transport

Four questions, in the order that eliminates the most options soonest.

TL;DR

Every answer is written out below rather than hidden behind the one you pick.

  1. 01Are you on Cloudflare at all?

    If not, Cloudflare Email Service is unavailable and the field is SES, Resend, SendGrid, Postmark or your own relay. If yes, it is the default for a reason: no third-party account, no extra credential, and the events come back on a Queues subscription. The domain has to already be on the same Cloudflare account, and onboarding happens in Cloudflare’s dashboard rather than here — it writes every DNS record itself, so there is nothing to copy.

  2. 02Do you send attachments over 5 MiB?

    Then Cloudflare Email Service is out for those messages — it caps at 5 MiB, and 25 MiB only to verified destinations. SES and Resend accept 40 MB; a raw relay is capped at 25 MiB here whatever your relay would take. Route the heavy sending domain elsewhere rather than capping your whole product, and remember the ceiling is measured after base64.

  3. 03Do you need bounce and complaint data?

    Everyone does, eventually. That rules out raw SMTP as a primary transport and means configuring SES properly rather than minimally: an SNS configuration set at setup time, not after the first campaign. On SES it also means asking AWS for production access — a new account is in the sandbox, which delivers only to addresses you have verified, at 200 messages a day, and looks exactly like a broken instance until you know that.

  4. 04What does it cost at your volume?

    Below a few thousand a month this question does not decide anything: the fixed floor dominates and a vendor free tier is genuinely cheaper. Above a hundred thousand it decides everything, and the answer is usually SES by a wide margin. The panel below runs the pricing page’s own functions rather than restating them.

COST BY VOLUME

Runs the same selfHostedCost, sesProviderCost, resendCost and sendgridCost functions the pricing page uses.

Everything below is monthly, in US dollars, and excludes your own time.
MailySend on your Cloudflare$40.15
Amazon SES as the transport$16.20
Resend$90.00
SendGrid$60.00

The self-hosted figure is an infrastructure bill, not a plan price — there is no upgrade tier to buy. Below a few thousand messages a month the fixed floor dominates and a vendor's free tier is genuinely cheaper; the crossover is the number worth knowing, and it is in the curve rather than in anyone's marketing.

A million messages a month, four ways

$114SES as the transport
$363Self-hosted on Workers
$600SendGrid
$650Resend
PathHow the price is shaped
Self-hosted on WorkersWorkers Paid at $5 a month, the first 3,000 messages included, then $0.35 per further thousand, plus storage, queues and analytics — cents below 20,000 messages, around $1.20 at 100,000, around $9 at a million.
A domain pointed at SESSwaps the metered send rate for $0.10 per thousand with no included allowance, and keeps the same $5 floor.
ResendA plan ladder rather than a rate: free to 3,000, $20 to 50,000, $90 to 100,000, then roughly $0.65 per thousand.
SendGridFlattens to $0.60 per thousand over a floor just under $20.

Which is why the fourth question decides nothing at 5,000 and decides everything at 500,000.

What a migration actually breaks

What movesWhat happens
The size ceilingA message that has been sending fine for a year starts failing permanently the day you move from SES to Cloudflare.
The DNS record setA domain bound to a transport gets that transport’s rows and nothing else, and rewriting them resets the domain to not_started and clears its last-verified timestamp — so there is a publish-and-verify step in the middle of the migration whether you planned one or not.
The return pathFrom cf-bounce.yourdomain.com to an SES MAIL FROM subdomain to Resend’s send.yourdomain.com. Bounce collection has to be re-established rather than inherited.
The signatureCloudflare and Resend sign with their own keys under their own selectors, SES signs with the key MailySend generated, and a raw relay signs with nothing at all — so MailySend’s own ms1 record has to be published and correct before an SMTP cutover, not after.

In roughly the order they surprise people.

The way to do it is one domain at a time. Routing is per sending domain, so a migration does not have to be a cutover: publish the new transport’s records, verify them, move one low-volume domain, watch its delivery and bounce rates for a few sends, then move the rest. Moving everything at once means every failure mode above arrives on the same afternoon, and you will not know which one you are looking at.

04What failover actually protects against

Failover is genuinely useful and is routinely expected to do something it cannot.

TL;DR

It covers a transport being unreachable. It does not cover a transport refusing you.

SES is returning 5xx because of an outage
  • Moving to a second transport is exactly right
  • The messages go out
SES is deferring you because your complaint rate rose
  • Moving to a second transport introduces a reputation problem to a sending identity that had nothing wrong with it
  • You now have two damaged reputations and the same underlying list

The router enforces that rule rather than trusting it. Every send error is classified into one of six kinds, and only two of them are allowed to reach a second transport.

ClassificationWhat the router doesWhy
permanentStop. No retry, no failover.A message that will never be accepted as written is not accepted anywhere else either.
transientRetry here, then a second transport.One of the two kinds that may fail over.
throttledWait the indicated delay, then failover is fine.The other one.
authStop and alert.Credentials do not improve with a retry.
suppressedStop.Mirrored inward as a suppression of our own.
unknownRetry here at most. Never a second transport.The message may already be on the wire.

Two attempts, not five. A routed send tries at most two transports by default. A third rarely helps, and every additional attempt widens the window in which a duplicate could occur. Pinning a transport on the send call disables failover entirely, which is the point of pinning — silently using another would violate an explicit instruction. Underneath, a per-transport circuit breaker opens after five consecutive failures and half-opens after thirty seconds, letting one request through to find out whether the transport recovered. Its job is not to protect the provider; it is to stop the whole queue’s retry budget being spent on a transport that is currently down while the healthy one sits idle.

FAILOVER NEEDS DNS YOU PUBLISHED IN ADVANCE
A second transport is only useful if its records are already in your zone. That is why a domain with no transport bound to it shows the union of every configured provider’s records, with the apex SPF includes merged into one legal record. Discovering at failover time that the fallback transport was never authorised for your domain is discovering it too late.

The other thing worth knowing: routing is per sending domain. Keeping marketing volume on a different domain from transactional mail — and therefore, if you want, on a different transport — is the standard way to stop a campaign’s complaint rate from affecting whether password resets arrive.

What just happened

You have picked a transport for reasons you can state: a message ceiling that fits what you actually send, an event story you can live with, and a price you have seen at your volume. The two facts most likely to bite later are that the size ceiling is per transport — a message that sends today can fail after a switch — and that SMTP reports no events at all.

Common questions

Read next

YOUR ACCOUNT, YOUR MAIL

Nothing to sign up for. Just deploy it.

Every guide on this site describes software you run yourself.