# Send email from Cloudflare Workers

Run a Cloudflare Worker locally with credential checks, validated input, and bounded simulated sending, then plan a provider API connection.

Author: Stampwing
Canonical: https://www.stampwing.com/guides/send-email-cloudflare-workers
Published: 2026-10-05
Reviewed: 2026-10-05
Category: Engineering

Run a local Worker and compare accepted, rejected and timed-out simulated requests.

## Short answer

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#short-answer

Call your chosen email provider’s authenticated HTTPS API from Worker code, with credentials stored as server-side secrets. Validate the caller and payload before creating a send. Bound the request, and preserve an uncertain outcome when a timeout prevents you from knowing whether the provider accepted it.

## Before you start

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#before-you-start

For: Developers building and operating application email.

Bring: Node.js 22+, npm, and the pinned Wrangler development dependency. Local runs need no Cloudflare account.

Scope: The download runs locally with synthetic data. All email delivery is simulated; no provider credentials or live sends are needed.

## Keep the email boundary inside the Worker

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#runtime-boundary

A Worker can accept a product event and turn it into a provider API request, but the browser should not receive the provider key. The destination, sender, template, and operation identity should come from an authorized business action rather than arbitrary request fields.

This lab accepts one message field and uses a fixed synthetic destination. A local bearer credential exercises the request boundary. It is intentionally not a complete user authentication or permission system, and the transport never calls an email provider.

Choose an adapter supported by your provider and runtime. A familiar Node SMTP example may rely on APIs that are not available in the same way in Workers. An HTTPS API keeps this tutorial’s integration boundary explicit.

## Start the Worker without deploying it

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#run-locally

Unzip the project and run npm ci, npm test, and npm run demo. The Node tests exercise the exported fetch handler with Web Request and Response objects. The separate interactive run uses Wrangler’s local Worker runtime.

Copy .dev.vars.example to .dev.vars, run npm run dev, and use the README’s curl command against http://127.0.0.1:3038. The included credential is an obvious fixture value. Do not put it in a deployed endpoint. Wrangler runs locally and the package has no deploy script.

Change TRANSPORT_MODE in wrangler.jsonc to rejected or timeout and restart the local run to see the failure responses. Successful output says the simulator accepted the request and that no email was sent.

Run from the extracted example folder

```sh
npm ci
npm test
npm run demo
```

## Read the key part of the example

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#example-source

This excerpt comes from worker.mjs, lines 19–48, in the download. It shows the central decision; run the complete package with the commands above.

Local simulation · worker.mjs

```javascript
    if (request.method !== "POST") return reply(405, "Use POST.");
    if (
      !["accepted", "rejected", "timeout"].includes(
        env.TRANSPORT_MODE ?? "accepted",
      )
    )
      return reply(503, "Unknown local transport mode.");
    if (typeof env.LAB_SECRET !== "string" || !env.LAB_SECRET)
      return reply(503, "Configure the local lab secret first.");
    if (request.headers.get("authorization") !== `Bearer ${env.LAB_SECRET}`)
      return reply(401, "Use the local lab credential.");
    if (!isJson(request)) return reply(415, "Use JSON.");
    let data;
    try {
      data = JSON.parse(await readBody(request, 8192));
    } catch (error) {
      return reply(
        error.status || 400,
        error.status ? error.message : "Check your request.",
      );
    }
    if (
      !data ||
      Array.isArray(data) ||
      typeof data.message !== "string" ||
      data.message.trim().length < 10 ||
      data.message.length > 4000
    )
      return reply(400, "Write a message between 10 and 4,000 characters.");
```

## Find the files you’ll change

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#example-files

Pinned dependencies: wrangler 4.45.0. Install with npm ci so the lockfile controls the resolved versions.

| File | Purpose |
| --- | --- |
| worker.mjs | The example behavior shown in this article. |
| test.mjs | Acceptance checks and synthetic failure cases. |
| demo.mjs / expected-output.json | A repeatable local experiment and its recorded result. |
| README.md | Setup commands, expected behavior and production boundaries. |
| BUILD-BRIEF.md | The coding-agent brief below. |
| package.json / package-lock.json | Pinned dependencies and runnable commands. |
| LICENSE | MIT license for adapting this example. |

## Compare the recorded local result

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#recorded-experiment

Captured from this package’s demo command on October 5, 2026. These results use synthetic fixtures and simulated delivery; they do not measure a live provider or inbox placement.

Recorded local simulation · expected-output.json

```json
{
  "simulated": true,
  "message": "The local simulator accepted the request. No email was sent.",
  "id": "simulated-worker-42"
}
```

## Bound the call without inventing an outcome

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#timeouts

The example gives its simulated transport a 100 ms deadline so the timeout path is quick to reproduce. That number is a lab setting, not a production recommendation. Pick a real bound based on your workload, provider, and execution budget.

When the deadline fires, the response is uncertain. A client disconnect, timeout, or lost response does not establish that the remote provider rejected the message. A live integration needs a stable logical operation ID and a reconciliation path before retrying.

The timer is cleaned up after every request. Missing credentials, invalid JSON, oversized bodies, and short messages fail before the simulated transport is reached.

## Use a durable job for work that must survive the response

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#durable-work

If email sending must continue after the user-facing request returns, persist the work and let a durable queue consumer perform it. Keep the queue’s retries separate from the provider’s own delivery attempts and from webhook redelivery.

A runtime extension such as waitUntil can be useful for bounded follow-up work, but it does not replace durable job storage. An in-memory promise can disappear with the runtime. Decide what evidence the app can honestly return before acknowledging the user’s action.

The local example does not provide that queue. It keeps the boundary small enough to inspect: one validated request, one simulated attempt, and one explicitly qualified outcome.

## Connect your provider and application authorization

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#production-setup

Store the real provider key as a Worker secret. Configure a verified sender, an explicit provider endpoint, and allowed templates on the server. Translate your internal email object into the provider’s documented request and response fields; never accept a provider URL from the browser.

Replace the fixture credential with your real authentication and authorization checks. Add shared rate limiting, abuse controls, bounded input sizes, and durable operation tracking. Map provider acceptance to acceptance language in your UI, not an assertion of inbox arrival.

Keep local tests on the simulator when you add an adapter in your own application. Use provider documentation and a separate controlled validation process for the live configuration. The ZIP contains no live adapter or provider credentials.

[Plan retry delays within the useful message lifetime](https://www.stampwing.com/tools/email-retry-calculator)

[Understand the transactional API boundary](https://www.stampwing.com/guides/transactional-email-api)

## When does a Worker need a queue?

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#common-question

Use durable jobs when the work must survive a request ending, support controlled retries, or await reconciliation. A short HTTP request alone cannot preserve pending work across a process failure. Keep a logical operation ID and original-provider evidence before retrying an uncertain result.

## Use with your coding agent

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#coding-agent-brief

Download the example, then copy this brief into your coding agent. The same brief is included as BUILD-BRIEF.md.

Coding-agent build brief

```markdown
# Send email from Cloudflare Workers — coding-agent brief

## Objective

Run a local Wrangler Worker with server-side authorization, bounded JSON input and a simulated HTTPS-provider boundary.

## Read first

Read README.md, package.json, worker.mjs, test.mjs and demo.mjs. Keep dependency versions pinned to package-lock.json. Use Node.js 22+.

## Dependencies

wrangler 4.45.0. Install with npm ci and preserve the lockfile.

## Work

Start at `fetch` in worker.mjs. Run the existing synthetic fixtures before changing behavior. Preserve the guide's original scenario and add a regression check for each changed failure case.

## Constraints

All email is simulated. Do not add provider credentials, send email, deploy services, or use the application's database. Preserve unknown outcomes instead of claiming delivery. Do not turn the local demonstration into production authentication or a public mail relay. Explain the production setup separately.

## Verification

Run `npm ci`, `npm test`, and `npm run demo`.

## Acceptance

Test missing and wrong credentials, lookalike JSON media types, oversized and stalled bodies, unknown modes, explicit rejection and timeout uncertainty. Run Wrangler locally with the example secret file; no deployment or real provider calls.
No real tokens, signing secrets, or recipient data appear in logs. All fixtures use synthetic values.
```

## Check the download

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#download-integrity

The ZIP contains 18,522 bytes. Compare its SHA-256 digest with this value before extracting. Downloads are free and require no signup.

SHA-256 · send-email-cloudflare-workers.zip

```text
6a484ff0c1c47ad08be13feecd03ff8509cd8e0d97e68bc553dce67750419eb2
```

## Sources

Section link: https://www.stampwing.com/guides/send-email-cloudflare-workers#sources

- [Cloudflare: local Worker development](https://developers.cloudflare.com/workers/development-testing/)
- [Cloudflare: Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/)
- [Cloudflare: sending email through an HTTPS provider API](https://developers.cloudflare.com/workers/tutorials/send-emails-with-resend/)
- [Cloudflare: Queues](https://developers.cloudflare.com/queues/)

Example download: [Download the local example](https://www.stampwing.com/downloads/send-email-cloudflare-workers.zip)

## Related guides

- https://www.stampwing.com/guides/transactional-email-api
- https://www.stampwing.com/guides/prevent-duplicate-emails
- https://www.stampwing.com/guides/test-transactional-email

Editorial policy: https://www.stampwing.com/resources/editorial
