docs: add Resend DNS-only proxy warning to send-mail config Resend domain verification CNAME records must use DNS-only (gray cloud) on Cloudflare. Proxied (orange cloud) records prevent verification, and a single failed attempt can take hours before retry. This is a recurring issue (#515) that the Resend setup docs did not warn about. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
7.1 KiB
Configure Email Sending
::: tip 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" },
]
[!warning] Important The binding name must be
SEND_MAIL— different from Cloudflare's officialSEND_EMAILexample.
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.
[!WARNING] 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]inwrangler.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_TOKEN
If 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_XYZ
Send 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
[!warning] Important The JSON key (e.g.
your-domain.comin the example below) must be replaced with your own domain — the domain configured in yourDOMAINSvariable. 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": "user@domain-a.com", "password": "xxx" }
},
"domain-b.com": {
"host": "smtp.gmail.com",
"port": 465,
"secure": true,
"authType": ["plain", "login"],
"credentials": { "username": "user@gmail.com", "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]inwrangler.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_CONFIG
Send Balance Mechanism
Users need a 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, users can still click "Request Send Permission" in the frontend to create a pending send-access record for admins to review - 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_BALANCEonly inserts an initial quota for addresses that do not yet have anaddress_senderrow (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 (
verifiedAddressListhit) 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.