Deliverability12 min read

Bounce Handling & List Hygiene: The Deliverability Maintenance That Prevents Domain Reputation Death

Keep bounce rates below 2% and protect sender reputation with proper bounce handling, list hygiene automation, and engagement-based pruning. A production-ready guide to avoiding the inbox-to-spam death spiral.

R

React Emails Pro

March 1, 2026

Your bounce rate is 8%. Gmail just flagged your domain. Your perfectly crafted password reset emails are going straight to spam—not because of bad content, but because you're still sending to dead addresses.

Inbox providers track bounce rates as a core reputation signal. Keep sending to invalid addresses, and they'll assume you're a spammer who bought a list. This is a practical guide to bounce handling and list hygiene that protects your sender reputation.

Industry benchmark: keep bounce rates below 2%. Above 5% and you're in the danger zone. Above 10% and major providers will start blocking you.

Why bounce rate kills deliverability

Bounce rate is the percentage of emails that fail to deliver. Every major email provider (Gmail, Outlook, Yahoo) tracks it and uses it to judge sender quality.

The logic is simple: legitimate senders maintain clean lists. They remove invalid addresses. They honor opt-outs. They know who their users are.

Spammers don't. They buy lists, scrape websites, and spray emails at anything that looks like an address. High bounce rates are a smoking gun.

Bounce rate directly affects domain reputation. Once your reputation drops, even valid emails to engaged users start landing in spam. Recovery can take weeks or months.

Hard bounces vs soft bounces

Not all bounces are equal. Your email provider returns different error codes depending on why delivery failed.

Hard bounces (permanent failures)

These addresses will never receive email. Remove them immediately.

  • Address doesn't exist: User typo, deleted account, fake email
  • Domain doesn't exist: "user@exmple.com" (typo) or defunct company domain
  • Recipient server rejects: Some corporate email systems block external senders entirely
hard-bounce-error.log
550 5.1.1 The email account that you tried to reach does not exist.
554 5.7.1 Service unavailable; Client host [IP] blocked
550 5.1.10 RESOLVER.ADR.RecipientNotFound; Recipient not found
Action: Suppress hard bounces permanently. Never send to them again. Most email service providers (Resend, SendGrid, Postmark) do this automatically, but always verify in your dashboard.

Soft bounces (temporary failures)

These might resolve. The address exists, but delivery failed for a temporary reason.

  • Mailbox full: User hasn't cleared their inbox
  • Server temporarily unavailable: Recipient mail server is down or overloaded
  • Message too large: Email exceeds recipient server's size limit
  • Greylisting: Spam defense technique that temporarily rejects first-time senders
soft-bounce-error.log
452 4.2.2 The email account that you tried to reach is over quota
421 4.7.0 Try again later, closing connection
552 5.2.3 Message size exceeds maximum allowed
Action: Retry soft bounces 2-3 times over 24-72 hours. If they persist after multiple attempts, treat them as hard bounces and suppress.

Complaint rate: the silent killer

Bounces aren't the only metric that matters. Complaint rate(spam reports) is just as critical—and harder to track.

When a user marks your email as spam, Gmail and Outlook record it. They don't tell you. But they do use it to decide whether future emails from your domain should land in the inbox.

Industry benchmark: keep complaint rates below 0.1% (1 complaint per 1,000 emails). Above 0.3% and you're at risk of being blocklisted.

How to reduce complaints

  • Clear unsubscribe: Make it easy to opt out. Hidden or missing unsubscribe links drive spam reports.
  • Send expected emails only: Don't email users who didn't explicitly opt in. No "we noticed you visited our site" emails.
  • Segment aggressively: Don't send billing emails to free users. Don't send product updates to churned accounts.
  • Frequency caps: Sending 5 emails in one day? Users will hit spam to make it stop.
Use the List-Unsubscribe header. Gmail and Outlook show a native unsubscribe button at the top of the email—users click it instead of marking as spam. See our List-Unsubscribe guide for implementation.

List hygiene: the maintenance checklist

List hygiene isn't a one-time fix. It's an ongoing practice. Here's the minimum viable maintenance routine for keeping your sender reputation clean.

1. Remove hard bounces immediately

Most email service providers suppress these automatically. If you're self-hosting or using SMTP directly, you need to build this yourself.

lib/email/bounce-handler.ts
import { prisma } from "@/lib/db";

export async function handleBounce(event: BounceEvent) {
  const { email, bounceType } = event;

  if (bounceType === "Permanent") {
    // Hard bounce: suppress permanently
    await prisma.user.update({
      where: { email },
      data: {
        emailSuppressed: true,
        suppressedAt: new Date(),
        suppressionReason: "hard_bounce",
      },
    });

    console.log(`[Bounce] Suppressed hard bounce: ${email}`);
  } else if (bounceType === "Transient") {
    // Soft bounce: increment retry counter
    await prisma.user.update({
      where: { email },
      data: {
        softBounceCount: { increment: 1 },
        lastSoftBounceAt: new Date(),
      },
    });

    // If soft bounces persist, treat as hard bounce
    const user = await prisma.user.findUnique({ where: { email } });
    if (user && user.softBounceCount >= 3) {
      await prisma.user.update({
        where: { email },
        data: {
          emailSuppressed: true,
          suppressedAt: new Date(),
          suppressionReason: "persistent_soft_bounce",
        },
      });

      console.log(`[Bounce] Suppressed persistent soft bounce: ${email}`);
    }
  }
}

2. Honor unsubscribes instantly

When a user clicks unsubscribe, stop sending immediately. Not "within 10 business days." Not "after one final email." Right now.

app/api/unsubscribe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";

export async function POST(req: NextRequest) {
  const { email } = await req.json();

  await prisma.user.update({
    where: { email },
    data: {
      emailOptedOut: true,
      optedOutAt: new Date(),
    },
  });

  // Also suppress from all marketing lists
  await prisma.emailList.deleteMany({
    where: { email },
  });

  return NextResponse.json({ success: true });
}
Sending to users who unsubscribed is a CAN-SPAM violation(US) and a GDPR violation (EU). It's not just bad for reputation—it's illegal.

3. Remove inactive users (engagement-based pruning)

If a user hasn't opened an email in 6-12 months, they've either abandoned that address or they're ignoring you. Either way, continuing to send hurts deliverability.

Inbox providers use engagement signals (opens, clicks, replies) to decide if users want your emails. Low engagement = spam folder.

scripts/prune-inactive-users.ts
import { prisma } from "@/lib/db";

async function pruneInactiveUsers() {
  const sixMonthsAgo = new Date();
  sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);

  // Find users with no email opens in 6 months
  const inactiveUsers = await prisma.user.findMany({
    where: {
      lastEmailOpenedAt: {
        lt: sixMonthsAgo,
      },
      emailOptedOut: false,
      emailSuppressed: false,
    },
  });

  console.log(`Found ${inactiveUsers.length} inactive users`);

  // Suppress or archive them
  for (const user of inactiveUsers) {
    await prisma.user.update({
      where: { id: user.id },
      data: {
        emailSuppressed: true,
        suppressionReason: "inactive_6mo",
      },
    });
  }

  console.log(`Suppressed ${inactiveUsers.length} inactive users`);
}

pruneInactiveUsers();
Before removing, send a re-engagement campaign: "We haven't heard from you in a while. Want to stay subscribed?" If they don't respond, suppress them.

4. Validate emails at signup

Catch typos and fake addresses before they enter your database. This prevents bounces entirely.

lib/email/validator.ts
import { z } from "zod";

const emailSchema = z.string().email();

// Basic format validation
export function isValidEmailFormat(email: string): boolean {
  return emailSchema.safeParse(email).success;
}

// Common typo detection
const commonDomainTypos: Record<string, string> = {
  "gmial.com": "gmail.com",
  "gmai.com": "gmail.com",
  "gnail.com": "gmail.com",
  "yahooo.com": "yahoo.com",
  "hotmial.com": "hotmail.com",
  "outlok.com": "outlook.com",
};

export function suggestEmailCorrection(email: string): string | null {
  const [localPart, domain] = email.split("@");
  if (!domain) return null;

  const correctDomain = commonDomainTypos[domain.toLowerCase()];
  if (correctDomain) {
    return `${localPart}@${correctDomain}`;
  }

  return null;
}

// Disposable email detection (basic version)
const disposableDomains = [
  "tempmail.com",
  "10minutemail.com",
  "guerrillamail.com",
  "mailinator.com",
  "throwaway.email",
];

export function isDisposableEmail(email: string): boolean {
  const domain = email.split("@")[1]?.toLowerCase();
  return disposableDomains.includes(domain);
}

For production use, consider a validation service like ZeroBounce or Kickbox for real-time verification at signup.

5. Double opt-in for marketing emails

Require users to confirm their email address before adding them to marketing lists. This ensures:

  • The email address is valid (no typos)
  • The user actually wants your emails (no accidental signups)
  • You comply with GDPR consent requirements
Double opt-in reduces list size but massively improves quality. Smaller, engaged list = better deliverability than a large, unengaged list.

Monitoring: how to track bounce rate

Most email service providers give you bounce metrics in their dashboard. Here's what to watch:

Resend

Navigate to Analytics → Bounce Rate. Track:

  • Overall bounce rate (should be < 2%)
  • Hard bounce count (remove these addresses)
  • Soft bounce count (monitor for patterns)

SendGrid

Activity Feed → Bounces. Filter by bounce type and download the list of bounced addresses. Cross-reference with your database to suppress.

Postmark

Activity → Bounces. Postmark automatically suppresses hard bounces, but you should still sync the suppression list to your app database to avoid re-adding them.

Set up webhook alerts for bounce events. Don't wait for the dashboard—handle bounces in real-time as they occur.
app/api/webhooks/resend/route.ts
import { NextRequest, NextResponse } from "next/server";
import { handleBounce } from "@/lib/email/bounce-handler";

export async function POST(req: NextRequest) {
  const event = await req.json();

  if (event.type === "email.bounced") {
    await handleBounce({
      email: event.data.email,
      bounceType: event.data.bounce_type, // "Permanent" or "Transient"
    });
  }

  return NextResponse.json({ received: true });
}

Recovery: what to do after reputation damage

If your bounce rate spiked and your domain reputation tanked, here's the recovery playbook:

  1. Stop sending immediately. Continuing to send to bad addresses makes it worse.
  2. Audit your list. Remove all hard bounces, soft bounces, and inactive users.
  3. Start slow. Gradually ramp up sending volume over 2-4 weeks. Sudden volume changes look suspicious.
  4. Warm up the domain. If reputation is severely damaged, consider using a new subdomain (e.g., mail2.yourapp.com) and warming it up from scratch.
  5. Monitor closely. Check bounce rate, complaint rate, and inbox placement daily during recovery.
Domain reputation recovery can take weeks or months. Prevention is 100x easier than recovery. Maintain list hygiene from day one.

Automation: set it and (mostly) forget it

Manual list hygiene doesn't scale. Here's the minimum automation setup for production apps:

  • Webhook handlers: Process bounce and complaint events in real-time
  • Scheduled jobs: Monthly/quarterly pruning of inactive users
  • Suppression sync: Daily sync of ESP suppression list to app database
  • Alerts: Slack/email notifications when bounce rate exceeds threshold
lib/cron/list-hygiene.ts
import { prisma } from "@/lib/db";

export async function monthlyListHygiene() {
  const stats = {
    inactiveUsers: 0,
    persistentSoftBounces: 0,
  };

  // Remove users inactive for 6 months
  const sixMonthsAgo = new Date();
  sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);

  const inactive = await prisma.user.updateMany({
    where: {
      lastEmailOpenedAt: { lt: sixMonthsAgo },
      emailSuppressed: false,
    },
    data: {
      emailSuppressed: true,
      suppressionReason: "inactive_6mo",
    },
  });

  stats.inactiveUsers = inactive.count;

  // Suppress persistent soft bounces
  const persistentBounces = await prisma.user.updateMany({
    where: {
      softBounceCount: { gte: 3 },
      emailSuppressed: false,
    },
    data: {
      emailSuppressed: true,
      suppressionReason: "persistent_soft_bounce",
    },
  });

  stats.persistentSoftBounces = persistentBounces.count;

  console.log(`[List Hygiene] Monthly cleanup complete:`, stats);

  return stats;
}
Use Vercel Cron (for Next.js) or a dedicated job queue (BullMQ, Inngest) to run list hygiene tasks automatically. Never rely on manual intervention.

Production checklist

Before launching transactional email, verify you have:

  • Bounce webhook handler (suppress hard bounces automatically)
  • Unsubscribe flow (instant opt-out, no delays)
  • Email validation at signup (catch typos early)
  • Scheduled list pruning (monthly/quarterly cleanup)
  • Bounce rate monitoring (alert if > 2%)
  • List-Unsubscribe header (reduce spam complaints)
  • Double opt-in for marketing (verify consent)

Get these right from the start, and you'll avoid the reputation damage that kills deliverability for most new senders.

Bottom line: List hygiene isn't optional. It's the foundation of deliverability. Clean lists = healthy reputation = inbox placement.

Production-ready templates for every flow

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates