Handling Bounces
Understand bounce types and how to handle them effectively.
What is a Bounce?
A bounce occurs when an email cannot be delivered to the recipient. The receiving mail server returns an error explaining why delivery failed.
Bounce Types
Hard Bounces
Permanent delivery failures that won't resolve with retries:
| Code | Reason | Example |
|---|---|---|
| 550 | User not found | Email address doesn't exist |
| 551 | User not local | Address rejected by server |
| 552 | Mailbox full (permanent) | Account abandoned |
| 553 | Invalid address format | Malformed email address |
| 554 | Transaction failed | Server permanently rejected |
Action: Immediately add to suppression list. Never send again.
Soft Bounces
Temporary failures that may resolve:
| Code | Reason | Example |
|---|---|---|
| 421 | Service unavailable | Server temporarily down |
| 450 | Mailbox busy | Try again later |
| 451 | Local error | Server processing issue |
| 452 | Insufficient storage | Mailbox full (temporary) |
Action: Retry with exponential backoff. Suppress after multiple failures.
Block Bounces
Your email was rejected due to content or reputation:
| Reason | Description |
|---|---|
| Spam content | Message flagged as spam |
| Blacklisted IP | Sending IP is on a blocklist |
| Policy violation | Message violates server policy |
| Rate limited | Too many emails sent |
Action: Review your content and sending practices.
Bounce Codes
SMTP Status Codes
Bounce responses include status codes:
5.1.1 - User unknown
5.2.2 - Mailbox full
5.7.1 - Rejected by policy
4.2.2 - Mailbox full (temporary)
First digit indicates severity:
4.x.x- Temporary failure (soft bounce)5.x.x- Permanent failure (hard bounce)
Enhanced Status Codes
More detailed codes provide specific reasons:
5.1.1 - Bad destination mailbox address
5.1.2 - Bad destination system address
5.2.1 - Mailbox disabled
5.2.2 - Mailbox full
5.7.1 - Delivery not authorized
Automatic Bounce Handling
How We Handle Bounces
- Delivery attempt - Email sent to recipient server
- Bounce received - Server returns error response
- Classification - We categorize as hard/soft/block
- Action taken - Retry (soft) or suppress (hard)
- Notification - Webhook sent to your server
Retry Logic for Soft Bounces
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | 0 | 0 |
| 2 | 5 min | 5 min |
| 3 | 30 min | 35 min |
| 4 | 2 hours | 2h 35m |
| 5 | 8 hours | 10h 35m |
After 5 failed attempts, treat as hard bounce.
Handling Bounces in Your App
Webhook Handler
app.post('/webhooks/email', async (req, res) => {
const event = req.body;
if (event.event === 'bounced') {
await handleBounce(event);
}
res.status(200).send('OK');
});
async function handleBounce(event: BounceEvent) {
const { to, bounceType, bounceCode, bounceDescription } = event;
if (bounceType === 'hard') {
// Permanently mark as undeliverable
await db.users.update({
where: { email: to },
data: {
emailStatus: 'invalid',
emailStatusReason: bounceDescription,
emailStatusUpdatedAt: new Date(),
},
});
// Notify relevant team
await notifySupport({
type: 'hard_bounce',
email: to,
reason: bounceDescription,
});
} else if (bounceType === 'soft') {
// Track soft bounces
await db.emailMetrics.upsert({
where: { email: to },
create: { email: to, softBounceCount: 1 },
update: { softBounceCount: { increment: 1 } },
});
// After too many soft bounces, treat as invalid
const metrics = await db.emailMetrics.findUnique({ where: { email: to } });
if (metrics?.softBounceCount >= 5) {
await markAsInvalid(to, 'Too many soft bounces');
}
}
}User Notification
Consider notifying users about delivery issues:
async function notifyUserOfDeliveryIssue(userId: string, email: string) {
// Show in-app notification
await createNotification({
userId,
type: 'email_delivery_failed',
message: `We couldn't deliver emails to ${email}. Please update your email address.`,
action: '/settings/email',
});
}Reducing Bounces
Validate at Sign-up
import { validate } from 'email-validator';
function validateEmail(email: string): boolean {
// Format validation
if (!validate(email)) {
return false;
}
// Check MX records
const hasMx = await checkMxRecords(email.split('@')[1]);
if (!hasMx) {
return false;
}
return true;
}Double Opt-in
Require email confirmation before sending:
- User signs up with email
- Send confirmation email
- User clicks confirmation link
- Only then add to mailing list
Regular List Cleaning
- Remove inactive subscribers
- Re-validate old addresses
- Remove role-based addresses (info@, support@)
Monitor Bounce Rates
| Rate | Status | Action |
|---|---|---|
| < 2% | Healthy | Continue monitoring |
| 2-5% | Warning | Review list quality |
| > 5% | Critical | Stop sending, clean list |
Bounce Analytics
View bounce metrics in your dashboard:
- Bounce rate over time
- Breakdown by type (hard/soft/block)
- Common bounce reasons
- Per-domain bounce rates
API Reference
// Get bounce details
const email = await client.emails.get(messageId);
if (email.status === 'bounced') {
console.log('Bounce type:', email.bounce.type);
console.log('Bounce code:', email.bounce.code);
console.log('Bounce reason:', email.bounce.description);
}
// List recent bounces
const bounces = await client.emails.list({
serverId: 'srv_123',
status: 'bounced',
limit: 100,
});Next Steps
- Suppressions - Manage suppression lists
- Best Practices - Improve deliverability
- Webhooks - Handle bounce events
On This Page
- What is a Bounce?
- Bounce Types
- Hard Bounces
- Soft Bounces
- Block Bounces
- Bounce Codes
- SMTP Status Codes
- Enhanced Status Codes
- Automatic Bounce Handling
- How We Handle Bounces
- Retry Logic for Soft Bounces
- Handling Bounces in Your App
- Webhook Handler
- User Notification
- Reducing Bounces
- Validate at Sign-up
- Double Opt-in
- Regular List Cleaning
- Monitor Bounce Rates
- Bounce Analytics
- API Reference
- Next Steps