Skip to content

Handle hard bounce emails

Detect and handle hard bounce emails to maintain sender reputation and manage undeliverable addresses

Handle hard bounce notifications to automatically remove invalid email addresses from your mailing lists and maintain good sender reputation.

What are hard bounces?

Hard bounces occur when an email cannot be delivered due to permanent reasons:

  • Invalid email address: The email address does not exist
  • Domain does not exist: The domain name is invalid or expired
  • Mailbox full: The recipient's mailbox has exceeded storage limits
  • Email blocked: The recipient's server permanently rejects emails

Configuration

Configure your worker to handle bounce notifications:

JSONC
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "bounce-handler",
// Set this to today's date
"compatibility_date": "2026-04-16",
"send_email": [
{
"name": "EMAIL"
}
],
"kv_namespaces": [
{
"binding": "SUPPRESSION_LIST",
"id": "your-kv-namespace-id"
}
]
}

Hard bounce detection

JavaScript
import * as PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
// Parse the raw email message
const parser = new PostalMime.default();
const rawEmail = new Response(message.raw);
const email = await parser.parse(await rawEmail.arrayBuffer());
// Check if this is a bounce notification
if (isBounceNotification(email)) {
const bounceInfo = await parseBounceInfo(email);
if (bounceInfo.type === 'hard') {
await handleHardBounce(bounceInfo, env);
console.log(`Hard bounce processed for: ${bounceInfo.originalRecipient}`);
return;
}
}
// Forward non-bounce emails normally
await message.forward('admin@yourdomain.com');
},
};
function isBounceNotification(email) {
// Check common bounce indicators
const subject = email.subject?.toLowerCase() || '';
const fromAddress = email.from?.address?.toLowerCase() || '';
// Common bounce indicators
const bounceSubjects = [
'mail delivery failed',
'undelivered mail returned to sender',
'delivery status notification',
'returned mail',
'mail system error'
];
const bounceFromPatterns = [
'mailer-daemon',
'mail-daemon',
'postmaster',
'noreply',
'bounce'
];
return bounceSubjects.some(phrase => subject.includes(phrase)) ||
bounceFromPatterns.some(pattern => fromAddress.includes(pattern));
}
async function parseBounceInfo(email) {
const text = email.text || '';
const html = email.html || '';
const content = text + ' ' + html;
// Extract original recipient email
const recipientMatch = content.match(/(?:to|for|recipient):\s*([^\s<]+@[^\s>]+)/i) ||
content.match(/([^\s<]+@[^\s>]+)/);
const originalRecipient = recipientMatch ? recipientMatch[1] : null;
// Determine bounce type based on content
const hardBounceIndicators = [
'user unknown',
'no such user',
'invalid recipient',
'recipient address rejected',
'mailbox unavailable',
'domain not found',
'5.1.1', // SMTP error code for bad destination mailbox
'5.1.2', // SMTP error code for bad destination system
'5.4.1', // SMTP error code for no answer from host
];
const isHardBounce = hardBounceIndicators.some(indicator =>
content.toLowerCase().includes(indicator.toLowerCase())
);
return {
type: isHardBounce ? 'hard' : 'soft',
originalRecipient,
reason: extractBounceReason(content),
timestamp: new Date().toISOString()
};
}
function extractBounceReason(content) {
// Extract the specific error message
const reasonPatterns = [
/diagnostic[- ]code:\s*(.+)/i,
/reason:\s*(.+)/i,
/error:\s*(.+)/i,
/(5\.\d+\.\d+[^.\n]*)/i
];
for (const pattern of reasonPatterns) {
const match = content.match(pattern);
if (match) {
return match[1].trim().split('\n')[0]; // Take first line only
}
}
return 'Unknown bounce reason';
}
async function handleHardBounce(bounceInfo, env) {
if (!bounceInfo.originalRecipient) {
console.log('Could not extract original recipient from bounce');
return;
}
// Add to suppression list in KV
await env.SUPPRESSION_LIST.put(
bounceInfo.originalRecipient,
JSON.stringify({
type: 'hard_bounce',
reason: bounceInfo.reason,
timestamp: bounceInfo.timestamp,
status: 'suppressed'
}),
{
metadata: {
bounceType: 'hard',
addedDate: bounceInfo.timestamp
}
}
);
console.log(`Added ${bounceInfo.originalRecipient} to suppression list: ${bounceInfo.reason}`);
}

Testing hard bounce handling

Create a test bounce notification:

Terminal window
curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \
--url-query 'from=mailer-daemon@example.com' \
--url-query 'to=bounce-handler@yourdomain.com' \
--header 'Content-Type: application/json' \
--data-raw 'From: Mail Delivery Subsystem <mailer-daemon@example.com>
To: bounce-handler@yourdomain.com
Subject: Mail delivery failed: returning message to sender
Date: Wed, 28 Aug 2024 10:30:00 +0000
Message-ID: <bounce123@example.com>
This message was created automatically by mail delivery software.
A message that you sent could not be delivered to one or more of its
recipients. This is a permanent error. The following address(es) failed:
nonexistent@example.com
SMTP error from remote mail server after RCPT TO:<nonexistent@example.com>:
host mx.example.com [192.168.1.1]: 550 5.1.1 User unknown
------ This is a copy of the message, including all the headers. ------
Return-path: <sender@yourdomain.com>
From: sender@yourdomain.com
To: nonexistent@example.com
Subject: Welcome to our service
Message-ID: <original123@yourdomain.com>
Welcome! Thanks for signing up.'

Checking suppression list

Add a utility function to check if an email is suppressed before sending:

JavaScript
async function isEmailSuppressed(email, env) {
const suppressionEntry = await env.SUPPRESSION_LIST.get(email);
if (suppressionEntry) {
const data = JSON.parse(suppressionEntry);
console.log(`Email ${email} is suppressed: ${data.reason}`);
return true;
}
return false;
}
// Use before sending emails
export async function sendEmail(recipient, subject, content, env) {
if (await isEmailSuppressed(recipient, env)) {
console.log(`Skipping email to suppressed address: ${recipient}`);
return { success: false, reason: "suppressed" };
}
// Proceed with email sending
// ... your email sending logic
}

Best practices

  1. Monitor bounce rates: Track bounce rates to maintain good sender reputation
  2. Automatic cleanup: Regularly review and clean suppression lists
  3. Double opt-in: Use double opt-in to reduce invalid addresses
  4. Retry logic: Implement appropriate retry logic for soft bounces
  5. Logging: Log all bounce handling for debugging and analytics

Next steps