How it works
What happens to an email between your API call and the recipient's inbox, and how its status gets back to you.
Flaresend is one Cloudflare Worker, flaresend. It is the only thing in your account that sends email. It keeps its records in D1, email bodies in R2, and moves work through four queues: flaresend-send, flaresend-events, flaresend-webhooks and a shared dead letter queue, flaresend-dlq.
202 queued1. The request
Your app calls POST /v1/emails with a project API key, or MailerRpc.send over a service binding. Both go through the same code, in this order. The first check that fails stops the request with an error, and nothing is stored.
- Parse. The body is checked against the schema: types, lengths, and that there is a
subject(unless a template is used) and one ofhtml,textortemplate. - Sender.
from(or the project's default sender) must be a valid address on one of the project's allowed domains, and in its allowed senders list if it has one. Otherwise403 invalid_sender. - Recipients. Every address in
to,ccandbccis parsed and lowercased, duplicates are dropped, and the total must be between 1 and 50. - Suppressions. With a live key, any recipient on the suppression list fails the request with
422 recipient_suppressed. Test keys skip this. - Headers. Custom headers are checked: no reserved names, no line breaks, size limits.
- Template. If
templateis set, it is rendered withdata. A template stored in the project wins over a built-in one with the same name. - Attachments and size. Attachment content must be base64, and the whole email must be under 5 MiB.
scheduledAt, if set, must be in the future and at most 30 days ahead. - Idempotency. If the request has an idempotency key that was used before with the same body, the first email is returned with
idempotent: trueand nothing else happens. See Idempotency. - Rate limits. With a live key, the per-project rate limit and the project's daily limit are checked.
Then Flaresend stores the email:
- The body (HTML, text, headers, attachments) goes to R2 at
payloads/{id}.json. An R2 lifecycle rule deletes it after 30 days. After that you can still see the email's record, but not its content, and it can't be resent. - The email, one row per recipient and the first timeline event go to D1 in one batch, so either all of them are written or none are.
- A message with the email ID goes on
flaresend-send.
The response is 202 with status: "queued". Flaresend does not wait for Cloudflare. With a test key, the email is recorded with status test and never queued.
2. Sending
The send queue consumer picks the message up, usually within a few seconds:
- It skips the email if it is no longer
queued,scheduledorsending(for example, it was canceled). - It moves the email to
sending. Only one queue message can do this, so a duplicate message never sends twice. - It loads the body from R2, adds the open pixel and click links if tracking is on, and calls Cloudflare Email Service.
- On success, it stores Cloudflare's
messageIdand sets the status tosent.
If Cloudflare returns an error, the consumer decides whether to try again:
| Error | What happens |
|---|---|
E_RATE_LIMIT_EXCEEDED, E_INTERNAL_SERVER_ERROR, E_DELIVERY_FAILED, network errors | Retried. The email goes back to queued and an email.retrying event is added. |
E_DAILY_LIMIT_EXCEEDED (your Cloudflare account's limit) | Retried after one hour. |
E_RECIPIENT_SUPPRESSED | Not retried. The email becomes rejected, and its recipients are added to the suppression list. |
| Anything else | Not retried. The email becomes failed and the error code is kept in lastError. |
Retries wait min(2^attempt × 15 s, 1 h) plus up to 20% random jitter: about 30 s, 1 min, 2 min, 4 min and so on, capped at an hour. After 8 retries the message moves to the dead letter queue, and the email is marked failed with the code max_retries_exhausted.
3. Delivery events
sent only means Cloudflare accepted the message. What happens next comes back as Cloudflare delivery events, one per recipient, on flaresend-events: delivered, deferred, bounced, failed, rejected or complained.
For each event, the events consumer:
- Drops duplicates. Events can arrive more than once. Each Cloudflare
eventIdis only applied once. - Finds the email by Cloudflare's
messageId. - Updates the recipient, but never downgrades it. A late
deferredcan't overwritedelivered, anddeliveredcan't overwrite a bounce. A complaint after delivery does replacedelivered. - Adds a timeline event, such as
email.deliveredoremail.bounced, with the SMTP details Cloudflare sent. - Recomputes the email's status from all its recipients, in this priority order:
complained>bounced>rejected>failed>delivered(only when every recipient is delivered) >deferred. Once an email has a failure status, a lower one never replaces it. - Updates suppressions. A hard bounce or a complaint puts the address on the suppression list, so the next send to it fails fast. The list is shared by every project on the mailer.
Events sometimes arrive before the send consumer has stored the messageId. Those events are retried a few times, then kept in an orphan_events table and applied as soon as the send finishes.
See Email statuses for what each status means.
Events need a subscription
Cloudflare only publishes delivery events for domains that have a queue subscription to flaresend-events. Without one, emails still send but stay at sent. See Deploy.
4. Webhooks
Every new timeline event is checked against your project's enabled webhooks. For each webhook subscribed to that event type, a delivery is created and put on flaresend-webhooks. The webhooks consumer POSTs the event JSON to your URL with a Flaresend-Signature header.
A delivery succeeds on any 2xx response within 10 seconds. Otherwise it is retried after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h and 12 h, then marked failed. See Retries.
Scheduled emails
An email with scheduledAt gets status scheduled and an email.scheduled event. Cloudflare Queues can delay a message by at most 12 hours, so:
- If it is due within 12 hours, it goes on the send queue straight away with a delay.
- If it is due later, a cron trigger that runs every 5 minutes puts it on the queue once it is within 12 hours of its time.
When the message arrives, the consumer checks scheduledAt again. If the email was rescheduled to more than a minute later, it waits again. A canceled email is skipped. See Scheduling.
The same cron also advances broadcasts (100 recipients per broadcast per run) and rolls up yesterday's analytics.
Two ways in
| HTTP | RPC | |
|---|---|---|
| Who can call it | Anything that can make an HTTPS request | Workers in the same Cloudflare account |
| Auth | Authorization: Bearer fs_live_… | None. The service binding is the auth, and you name the project |
| Client | new Flaresend({ apiKey, baseUrl }) | rpcClient(env.MAILER, { project }) |
| Methods | Everything in the API reference | send, sendBatch, get, list, cancel |
| Client retries | Yes, on 429, 5xx and network errors | No |
Both paths run the same checks and write the same records. See Send with Cloudflare Workers for the RPC setup.