Email sending
Test email sending Workers locally using wrangler dev with simulated email delivery
Test email sending functionality locally using wrangler dev to simulate email delivery and verify your sending logic before deploying.
- Sign up for a Cloudflare account ↗.
- Install
Node.js↗.
Node.js version manager
Use a Node version manager like Volta ↗ or nvm ↗ to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.
Configure your Wrangler file with the email binding:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "email-sending-worker", "compatibility_date": "2024-01-01", "send_email": [ { "name": "EMAIL" } ]}name = "email-sending-worker"compatibility_date = "2024-01-01"
[[send_email]]name = "EMAIL"Using remote bindings is the recommended way to develop with Email Service locally. By default, wrangler dev simulates the email binding locally -- emails are logged to the console but not actually sent. With remote bindings, your Worker runs locally but sends real emails through Email Service.
Set remote: true on the email binding in your Wrangler configuration:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "email-sending-worker", // Set this to today's date "compatibility_date": "2026-04-16", "send_email": [ { "name": "EMAIL", "remote": true } ]}name = "email-sending-worker"# Set this to today's datecompatibility_date = "2026-04-16"
[[send_email]]name = "EMAIL"remote = trueThen run wrangler dev as usual. Calls to env.EMAIL.send() will send actual emails through Email Service while your Worker code runs locally.
When running wrangler dev without remote bindings, the email binding is simulated locally. Emails are not sent -- instead, the email content is logged to the console and saved to local files for inspection.
export default { async fetch(request, env, ctx) { if (request.method !== "POST") { return new Response("Method not allowed", { status: 405 }); }
try { const emailData = await request.json();
console.log("📤 Sending email:", { to: emailData.to, from: emailData.from, subject: emailData.subject, });
const response = await env.EMAIL.send(emailData);
return new Response( JSON.stringify({ success: true, id: response.messageId, }), { headers: { "Content-Type": "application/json" }, }, ); } catch (error) { return new Response( JSON.stringify({ success: false, error: error.message, }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } },};Start your development server:
npx wrangler devSend a test email:
curl -X POST http://localhost:8787/ \ -H "Content-Type: application/json" \ -d '{ "to": "recipient@example.com", "from": "sender@yourdomain.com", "subject": "Test Email", "html": "<h1>Hello from Wrangler!</h1>", "text": "Hello from Wrangler!" }'Wrangler will show output like:
[wrangler:info] send_email binding called with MessageBuilder:From: sender@yourdomain.comTo: recipient@example.comSubject: Test Email
Text: /tmp/miniflare-.../files/email-text/<message-id>.txtThe email content (text and HTML) is saved to local files that you can inspect to verify your email structure before deploying.
Local development simulates the send_email binding locally, but ArrayBuffer values in attachment content cannot be serialized by the local simulator. If you pass an ArrayBuffer (for example, for image or PDF attachments), you will see an error like:
Cannot serialize value: [object ArrayBuffer]Workaround: Use string content for text-based attachments during local development. To test binary attachments (images, PDFs), deploy your Worker with npx wrangler deploy and test against the deployed version.
This limitation only affects local development — ArrayBuffer content works correctly on deployed Workers.
- Deploy your sending worker: Send emails get started
- See advanced patterns: Email sending examples