Sync recipient records
Synchronize application recipient records with Email Sending lifecycle events.
Use Email Sending event subscriptions to update application records after delivery problems. This example uses Cloudflare Queues and Workers KV to remove recipients from transactional notifications.
Before you begin:
- Enable an Email Sending domain.
- Create a Worker project.
- Create a Workers KV namespace.
Store each eligible recipient address as a key in KV. The value can contain notification preferences or related metadata.
- Email Sending publishes bounce and complaint events.
- A queue delivers those events to a Worker.
- The Worker removes ineligible recipient records from KV.
Remove records for every message.complained event. These events indicate that a recipient reported the message as spam.
Remove bounced records only when payload.bounce.type is "hard". Temporary failures produce message.deferred events while retries remain. Exhausted temporary retries can produce message.bounced events with a "soft" bounce type.
For payload details, refer to Available Email Sending events.
Create a queue and subscribe it to your sending domain:
-
In the Cloudflare dashboard, go to the Queues page. Create a queue named
Go to Queuesemail-events. -
Select
email-events, then select Subscriptions > Subscribe to events. -
Enter a subscription name and select Email Sending as the source.
-
Select your sending domain and the
message.bouncedandmessage.complainedevents. -
Select Subscribe.
Bind the KV namespace and register the Worker as the queue consumer:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "recipient-record-sync", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-07-15", "kv_namespaces": [ { "binding": "RECIPIENTS", "id": "<RECIPIENTS_KV_NAMESPACE_ID>" } ], "queues": { "consumers": [ { "queue": "email-events", "max_batch_size": 10, "max_retries": 3, "dead_letter_queue": "email-events-dlq" } ] }}name = "recipient-record-sync"main = "src/index.ts"# Set this to today's datecompatibility_date = "2026-07-15"
[[kv_namespaces]]binding = "RECIPIENTS"id = "<RECIPIENTS_KV_NAMESPACE_ID>"
[[queues.consumers]]queue = "email-events"max_batch_size = 10max_retries = 3dead_letter_queue = "email-events-dlq"The configuration creates email-events-dlq during deployment. Queues moves events there after three retries.
The queue() handler processes each event independently. It deletes applicable recipient records and retries failed KV operations.
export default { async queue(batch, env) { for (const message of batch.messages) { try { const event = message.body;
if (shouldRemove(event)) { await removeRecipient(env, event); }
message.ack(); } catch (error) { console.error("Failed to process Email Sending event", { eventId: message.body.payload.eventId, error, }); message.retry(); } } },};
function shouldRemove(event) { if (event.type === "cf.email.sending.message.complained") { return true; }
return ( event.type === "cf.email.sending.message.bounced" && event.payload.bounce?.type === "hard" );}
async function removeRecipient(env, event) { await env.RECIPIENTS.delete(event.payload.recipient); console.log("Removed recipient record", { eventId: event.payload.eventId, reason: event.type, });}interface Env { RECIPIENTS: KVNamespace;}
interface EmailSendingEvent { type: | "cf.email.sending.message.bounced" | "cf.email.sending.message.complained"; payload: { eventId: string; recipient: string; bounce?: { type: "hard" | "soft"; }; };}
export default { async queue(batch, env): Promise<void> { for (const message of batch.messages) { try { const event = message.body;
if (shouldRemove(event)) { await removeRecipient(env, event); }
message.ack(); } catch (error) { console.error("Failed to process Email Sending event", { eventId: message.body.payload.eventId, error, }); message.retry(); } } },} satisfies ExportedHandler<Env, EmailSendingEvent>;
function shouldRemove(event: EmailSendingEvent): boolean { if (event.type === "cf.email.sending.message.complained") { return true; }
return ( event.type === "cf.email.sending.message.bounced" && event.payload.bounce?.type === "hard" );}
async function removeRecipient( env: Env, event: EmailSendingEvent,): Promise<void> { await env.RECIPIENTS.delete(event.payload.recipient); console.log("Removed recipient record", { eventId: event.payload.eventId, reason: event.type, });}Deleting a missing KV key succeeds. This makes repeated event delivery safe.
The handler acknowledges each successful message. Failed operations move to the dead letter queue after three retries.
Deploy the Worker and its queue consumer configuration:
npx wrangler deploy yarn wrangler deploy pnpm wrangler deploy Monitor the dead letter queue for failed events. Reprocess them after fixing the underlying error.
- Event subscriptions — review event schemas.
- Suppression lists — understand automatic suppressions.
- Queues retries — control message retries.
- Workers KV consistency — account for propagation delays.