Configure Email Sending
Recommended
Use Cloudflare send_email binding as the default send channel. Bind SEND_MAIL and finish Email Routing onboarding, then the Worker can send to any external address directly.
Workers Paid includes 3,000 messages/month, then $0.35 per 1,000 messages.
Send Channel Priority
Each /api/send_mail request matches channels in order; the first hit sends:
| Order | Condition | Channel | Deducts balance |
|---|---|---|---|
| 1 | SEND_MAIL bound AND recipient in verifiedAddressList | Cloudflare binding (compat mode) | No |
| 2 | RESEND_TOKEN or RESEND_TOKEN_<DOMAIN> set | Resend API | Yes |
| 3 | SMTP_CONFIG has entry for current domain | worker-mailer SMTP | Yes |
| 4 | SEND_MAIL bound (none of the above) | Cloudflare binding (recommended primary) | Yes |
| — | None of the above | Throws | — |
NOTE
Binding send failures return an error directly.
Using the Cloudflare send_email Binding (Recommended)
Only available when deploying via CLI. Add to wrangler.toml:
# Send emails via the Cloudflare send_email binding
send_email = [
{ name = "SEND_MAIL" },
]Important
The binding name must be SEND_MAIL — different from Cloudflare's official SEND_EMAIL example.
After the following steps, you can send to any external address directly:
- Enable Email Routing on the domain in the Cloudflare Dashboard and complete onboarding
- Add the
send_emailbinding shown above towrangler.toml - Deploy the Worker
No additional env var is required.
Send Emails Using Resend
Register at https://resend.com/domains and add DNS records according to the instructions.
DNS record proxy status on Cloudflare
Resend domain verification CNAME records must be set to DNS-only (gray cloud) in the Cloudflare DNS dashboard. Proxied (orange cloud) records will prevent Resend from completing verification, and a single failed attempt can take several hours before Retry becomes available. See #515.
Create an api key on the API KEYS page.
Then execute the following command to add RESEND_TOKEN to secrets:
NOTE
If you find this troublesome, you can also put it directly in plain text under [vars] in wrangler.toml, but this is not recommended
If you deployed through the UI, you can add it under Variables and Secrets in the Cloudflare UI interface.
# Switch to worker directory
cd worker
wrangler secret put RESEND_TOKENIf you have multiple domains with different api keys, you can add multiple secrets in wrangler.toml, named RESEND_TOKEN_ + <UPPERCASE DOMAIN WITH . REPLACED BY _>, for example:
wrangler secret put RESEND_TOKEN_XXX_COM
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZSend Emails Using SMTP
The format of SMTP_CONFIG is as follows. The key must be your own sending domain, and the value is the SMTP configuration.
For SMTP configuration format details, refer to zou-yu/worker-mailer
Important
The JSON key (e.g. your-domain.com in the example below) must be replaced with your own domain — the domain configured in your DOMAINS variable. This is one of the most common configuration mistakes. Do not copy the example domain directly.
{
"your-domain.com": {
"host": "smtp.example.com",
"port": 465,
"secure": true,
"authType": [
"plain",
"login"
],
"credentials": {
"username": "your-smtp-username",
"password": "your-smtp-password"
}
}
}Field Reference:
| Field | Description |
|---|---|
key (e.g. your-domain.com) | Your sending domain, must match a domain configured in DOMAINS |
host | SMTP server address, e.g. smtp.mailgun.org, smtp.gmail.com, or your self-hosted SMTP server |
port | SMTP port, typically 465 (SSL) or 587 (STARTTLS) |
secure | Whether to use SSL/TLS. Set to true for port 465, false for port 587 |
authType | Authentication method, typically ["plain", "login"] |
credentials.username | SMTP server login username |
credentials.password | SMTP server login password |
If you have multiple domains using different SMTP services, add multiple keys in the same JSON:
{
"domain-a.com": {
"host": "smtp.mailgun.org",
"port": 465,
"secure": true,
"authType": ["plain", "login"],
"credentials": { "username": "[email protected]", "password": "xxx" }
},
"domain-b.com": {
"host": "smtp.gmail.com",
"port": 465,
"secure": true,
"authType": ["plain", "login"],
"credentials": { "username": "[email protected]", "password": "app-password" }
}
}Then execute the following command to add SMTP_CONFIG to secrets:
NOTE
If you find this troublesome, you can also put it directly in plain text under [vars] in wrangler.toml, but this is not recommended
If you deployed through the UI, you can add it under Variables and Secrets in the Cloudflare UI interface.
# Switch to worker directory
cd worker
wrangler secret put SMTP_CONFIGUser Accounts, Email Addresses, and Send Permission
These concepts are related, but they identify different objects:
| Concept | Description |
|---|---|
| User account | Signs in to the user center and can bind and manage multiple email addresses. The user account's login email is not automatically a sending or receiving address |
| Email address | The identity that actually receives and sends mail, such as [email protected]. The address currently selected on the frontend home page is authenticated by its Address JWT |
| Send permission and balance | Permission and balance records in address_sender are managed independently per email address; user roles configured by NO_LIMIT_SEND_ROLE can bypass address balance checks |
Multiple email addresses bound to the same user account therefore do not share send permission or balance. To request permission for an address:
- Switch to the email address that needs to send mail
- Open the Send Mail page
- Click Request Access
The request affects only the address selected at that time. After switching to another address, check that address's own balance and request permission separately if needed. An email address can also hold send permission without being bound to a user account.
Send Balance Mechanism
An email address needs its own send balance to send emails. The balance mechanism works as follows:
- Auto-initialize Default Quota: When
DEFAULT_SEND_BALANCE > 0, the system automatically initializes the default quota when the user opens the send page or calls the send-mail API for the first time - Manual Request: If
DEFAULT_SEND_BALANCE = 0, switch to the target email address in the frontend and click Request Access to create a pending send-access record for the current address - Unlimited Sending: The following methods can bypass balance checks:
- Add the address to the "No Limit Send Address List" in the admin console
- Configure the
NO_LIMIT_SEND_ROLEenvironment variable to specify roles that can send without limits
NOTE
DEFAULT_SEND_BALANCE only inserts an initial quota for addresses that do not yet have an address_sender row (ON CONFLICT DO NOTHING); existing rows — including admin-disabled or admin-edited ones — are never modified by the runtime path. Restoring a previously disabled or pre-existing address must go through the admin console (enable + set balance).
Layer 1 (verifiedAddressList hit) does not deduct balance, but it still counts toward send limits; layers 2/3/4 all deduct balance.
Send limits apply to all send channels, including admin send endpoints.
Daily and monthly windows are calculated in UTC.
The current limit implementation is a soft guard. It is suitable for routine quota control, but it should not be treated as a strict hard-stop cost gate under database errors or high concurrency.
Send Emails to Authenticated Forwarding Addresses on Cloudflare
Typical use case: non-onboarded domains or Workers free-tier users.
In this compatibility mode, mail is sent via SEND_MAIL binding only when the recipient is in the admin Verified Address List.