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:

CodeReasonExample
550User not foundEmail address doesn't exist
551User not localAddress rejected by server
552Mailbox full (permanent)Account abandoned
553Invalid address formatMalformed email address
554Transaction failedServer permanently rejected

Action: Immediately add to suppression list. Never send again.

Soft Bounces

Temporary failures that may resolve:

CodeReasonExample
421Service unavailableServer temporarily down
450Mailbox busyTry again later
451Local errorServer processing issue
452Insufficient storageMailbox full (temporary)

Action: Retry with exponential backoff. Suppress after multiple failures.

Block Bounces

Your email was rejected due to content or reputation:

ReasonDescription
Spam contentMessage flagged as spam
Blacklisted IPSending IP is on a blocklist
Policy violationMessage violates server policy
Rate limitedToo 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

  1. Delivery attempt - Email sent to recipient server
  2. Bounce received - Server returns error response
  3. Classification - We categorize as hard/soft/block
  4. Action taken - Retry (soft) or suppress (hard)
  5. Notification - Webhook sent to your server

Retry Logic for Soft Bounces

AttemptDelayTotal Time
100
25 min5 min
330 min35 min
42 hours2h 35m
58 hours10h 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:

  1. User signs up with email
  2. Send confirmation email
  3. User clicks confirmation link
  4. 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

RateStatusAction
< 2%HealthyContinue monitoring
2-5%WarningReview list quality
> 5%CriticalStop 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