Sending Email with Cloudflare SMTP—from Dashboard Setup to Nodemailer
A practical Cloudflare Email Service SMTP guide covering domain onboarding, API Tokens, cURL, Nodemailer, Python, limits, response codes, retries, and production security.

Cloudflare Email Service added authenticated SMTP in 2026. If an application already uses Nodemailer, Python smtplib, PHPMailer, or a product that only exposes SMTP settings, you can change the delivery provider without rebuilding the whole sending layer.
This is a standalone SMTP tutorial. It does not repeat the broader topics of Email Routing, inbound processing, or Email Workers. It has one concrete goal:
Connect to
smtp.mx.cloudflare.net:465, authenticate with a Cloudflare API Token over SMTPS, and send a transactional email from your own domain.
This article reflects the Cloudflare Email Service Beta visible on July 31, 2026. Beta quotas and dashboard screens can change, so recheck the official sending guide and SMTP API reference before production rollout.
The one-minute configuration
| Setting | Value |
|---|---|
| SMTP host | smtp.mx.cloudflare.net |
| Port | 465 |
| Encryption | Implicit TLS, also known as SMTPS |
| Username | The literal string api_token |
| Password | A Cloudflare API Token with Email Sending: Edit |
| Authentication | AUTH PLAIN or AUTH LOGIN |
| From address | An address on a domain enabled in Email Sending |
Three values are easy to confuse:
- The username is not your Cloudflare email or Account ID. It is always
api_token. - The password is not your Cloudflare login password. It is a dedicated API Token.
- Port 465 starts TLS immediately. A port 587 STARTTLS configuration is not interchangeable with it.
Cloudflare currently does not provide port 587 STARTTLS or outbound relay on port 25. Port 25 is reserved for inbound Email Routing. See the official SMTP reference for the current protocol boundary.
Prerequisites
1. Put the sending domain on Cloudflare DNS
Email Sending requires Cloudflare DNS. In the dashboard, open:
Cloudflare Dashboard
→ Compute
→ Email Service
→ Email Sending
Select the domain and finish onboarding. Cloudflare configures the DNS records used for sender authorization, DKIM, and bounce handling. Wait until the domain is marked configured before debugging SMTP; a valid login can still be rejected if the sender domain is not authorized.
2. Create a least-privilege API Token
Open the domain's Connect page and select SMTP. The dashboard offers to create an API Token with Email Sending: Edit.

Captured on July 31, 2026. The image contains only Cloudflare configuration values and an environment-variable placeholder. No real token was created, displayed, or included in this article.
Prefer an account-owned token, then narrow it as much as possible:
- Grant access only to the account that sends email.
- Grant only
Email Sending: Edit. - Set a sensible expiration.
- Add client IP filters when the deployment has stable egress addresses.
- Give every application its own token so it can be audited and revoked independently.
The complete token is shown only once. Store it in server-side secrets or runtime environment variables. Never:
- Commit it to Git, Markdown, or build logs.
- Put it in a
NUXT_PUBLIC_*variable. - Expose it to browser JavaScript.
- Log the SMTP transport configuration on errors.
- Share one long-lived token across unrelated applications.
Verify the path with cURL first
A minimal cURL test separates Cloudflare configuration errors from framework integration errors.
Create mail.txt:
From: Jackie Moon <noreply@sparkles-editor.com>
To: recipient@example.com
Subject: Cloudflare SMTP test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Hello,
This message was delivered through Cloudflare Email Service SMTP.
Read the token into the current shell without placing the value in command history:
read -s CF_EMAIL_SMTP_TOKEN
export CF_EMAIL_SMTP_TOKEN
Send the message:
curl --url "smtps://smtp.mx.cloudflare.net:465" \
--ssl-reqd \
--user "api_token:${CF_EMAIL_SMTP_TOKEN}" \
--mail-from "noreply@sparkles-editor.com" \
--mail-rcpt "recipient@example.com" \
--upload-file mail.txt
A successful SMTP transaction returns 250. Preserve the Message-ID from the response so it can be correlated with Email Service dashboard logs. Compare the command with Cloudflare's official cURL example.
If this step fails, do not start changing Nodemailer yet:
535: the token is invalid, expired, or lacksEmail Sending: Edit.550: the sender address or domain is not allowed.552: the message is too large.421/451: a temporary failure or rate limit; retry later.
Use Nodemailer from Node.js
This setup is for Node.js, Docker, a VPS, conventional serverless functions, or a Nitro server running in a Node-compatible environment.
Install the packages:
pnpm add nodemailer
pnpm add -D @types/nodemailer
Define server-side variables:
CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret
SMTP_FROM="Jackie Moon <noreply@sparkles-editor.com>"
Create the transport:
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: 'smtp.mx.cloudflare.net',
port: 465,
secure: true,
auth: {
user: 'api_token',
pass: process.env.CF_EMAIL_SMTP_TOKEN,
},
connectionTimeout: 10_000,
greetingTimeout: 10_000,
socketTimeout: 30_000,
})
const info = await transporter.sendMail({
from: process.env.SMTP_FROM,
to: 'recipient@example.com',
subject: 'Welcome to Signal Log',
text: 'Your account is ready.',
html: '<p>Your account is ready.</p>',
})
console.info('Email accepted', {
messageId: info.messageId,
accepted: info.accepted.length,
rejected: info.rejected.length,
})

Cloudflare's dashboard sample uses process.env.CLOUDFLARE_API_TOKEN. A project may use a more specific name, but the value must remain server-side.
Production code also needs an envelope boundary:
const allowedFrom = new Set([
'noreply@sparkles-editor.com',
'support@sparkles-editor.com',
])
function assertEmailEnvelope(from: string, to: string) {
if (!allowedFrom.has(from)) {
throw new Error('Sender is not allowed')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) {
throw new Error('Recipient is invalid')
}
}
Do not expose a public endpoint that accepts arbitrary from, to, subject, and HTML values. It can become an open relay or a phishing tool. A safer endpoint accepts business inputs such as userId and templateId; the server then loads a verified recipient and renders a whitelisted template.
What if Nuxt runs on Cloudflare Workers?
The runtime distinction matters.
Nodemailer targets conventional Node SMTP sockets. A Nuxt application already running on Cloudflare Workers should normally use the send_email binding:
interface Env {
EMAIL: SendEmail
}
await env.EMAIL.send({
from: 'noreply@sparkles-editor.com',
to: 'recipient@example.com',
subject: 'Verification code',
text: 'Your code is 123456',
})
This avoids storing an SMTP API Token in the Worker, and the binding carries its own permission boundary.
Use this decision table:
| Situation | Recommended entry point |
|---|---|
| Nuxt or an API is deployed on Cloudflare Workers | Workers send_email binding |
| An external service is comfortable with HTTPS and token management | REST API |
| An existing Node, Python, or PHP application already supports SMTP | Authenticated SMTP |
| A printer, legacy CMS, or third-party product only accepts SMTP settings | SMTP, after confirming port 465 SMTPS support |
| Browser-side sending | Never; use a controlled server endpoint |
All three server-side entry points feed Cloudflare's delivery pipeline, logs, and domain authentication. SMTP is about compatibility, not a requirement to move every new service back to sockets.
Minimal Python example
Python's standard library is sufficient:
import os
import smtplib
from email.message import EmailMessage
token = os.environ["CF_EMAIL_SMTP_TOKEN"]
message = EmailMessage()
message["From"] = "Jackie Moon <noreply@sparkles-editor.com>"
message["To"] = "recipient@example.com"
message["Subject"] = "Cloudflare SMTP test"
message.set_content("This email was sent over implicit TLS.")
with smtplib.SMTP_SSL("smtp.mx.cloudflare.net", 465, timeout=30) as smtp:
smtp.login("api_token", token)
smtp.send_message(message)
The important part is SMTP_SSL, not a plaintext connection followed by starttls().
Limits and attachments
According to the current Email Service platform limits:
- One email can have at most 50 combined recipients.
- An SMTP session accepts at most 50
RCPT TOcommands. - A normal message is limited to 5 MiB.
- A message to a verified destination can be up to 25 MiB.
- SMTP
AUTHtimes out after 30 seconds. - SMTP
DATAtimes out after 300 seconds. - Custom headers are limited to 16 KiB.
- A Subject is limited to 998 characters.
The message size includes the MIME-encoded body and attachments. Base64 commonly adds roughly one third to a binary attachment, so a 4.9 MiB file does not necessarily fit in a 5 MiB email.
Daily quota shown in the dashboard can vary by account, Beta stage, and usage. Do not hard-code a number observed once. Check the current dashboard and reserve capacity in your own queue.
Error handling and retries
SMTP response classes already indicate whether a retry makes sense:
| Response | Meaning | Action |
|---|---|---|
250 | Cloudflare accepted the message | Store the Message-ID and do not send it again |
421 / 451 | Temporary failure or throttling | Exponential backoff with jitter |
535 | Authentication failed | Stop automatic retries and inspect the token |
550 | Sender, domain, or recipient policy rejected the message | Fix configuration or data first |
552 | Message too large | Reduce the body or attachments |
A reasonable retry schedule is 30 seconds, 2 minutes, 10 minutes, and 30 minutes, with a fixed maximum. Every business send needs an idempotency key:
password-reset:{userId}:{resetRequestId}
invoice:{invoiceId}:issued
Queue temporary failures and route permanent failures to a failed or review state. Do not retry every non-250 response immediately, and do not send the same verification code ten times because the network was unstable.
Delivery does not end at 250
250 means Cloudflare accepted the message, not that it reached the final inbox. Production monitoring should include:
- Delivery, Bounce, Deferred, and Suppression results.
- Gmail, Outlook, QQ Mail, and other major destination outcomes.
- Alignment among From, Return-Path, DKIM, and DMARC.
- Immediate suppression of hard-bounce addresses.
- Complaint rate and invalid-address growth.
- Explicit expiry for verification and password-reset messages.
Transactional email should use a stable, recognizable sender and provide both plain text and HTML. Do not spoof Reply-To, use an authentication channel for marketing blasts, or place raw user input in email headers.
Production checklist
- The sender domain is configured in Email Sending.
- The client uses port 465 with implicit TLS.
- The username is exactly
api_token. - The token has only
Email Sending: Edit. - The token remains in server secrets, never Git, logs, or frontend code.
- From addresses come from a server-side allowlist.
- Recipients are validated, deduplicated, and length-limited.
- HTML comes from fixed templates and includes a text alternative.
-
421/451use jittered exponential backoff. -
535/550/552are not retried blindly. - Logs store Message-ID, template name, and idempotency key—not the token.
- Bounces, suppressions, complaints, and daily quota are monitored.
The final choice
For an established Node, Python, or PHP application, Cloudflare SMTP has a straightforward advantage: change the host, port, and credentials, then join Cloudflare's delivery and logging system.
For an application native to Workers, there is no need to emulate a traditional server. Prefer the Email binding. SMTP is a bridge to the existing ecosystem, not the only entrance.
Comments
Sign in to join the discussion
Your email remains private; only your display name appears.