How to ensure accurate bounce tracking across ESPs?

Still have a question, spotted an error, or have a better explanation or a source we should cite?

Bounce tracking falls apart across ESPs because nobody agrees on what a bounce is. SendGrid calls a 550 "5.1.1 user unknown" a hard bounce and drops the address into its suppression list. Mailgun classifies the same response as a permanent failure but exposes it through a different webhook event with different fields. Postmark splits hard bounces into 16 sub-types. Your CRM sees three different shapes for the same dead mailbox, and your suppression list ends up full of holes.

The fix is to stop trusting the ESP's classification and start reading the underlying SMTP response yourself.

Anchor on the SMTP response, not the ESP label

Every bounce carries a numeric SMTP reply code (5xx = permanent, 4xx = temporary) and, where the receiving server supports it, an enhanced status code in the form X.Y.Z defined by RFC 3463. A 5.1.1 means the mailbox does not exist. A 5.2.2 means it is full. A 4.2.1 means the mailbox is temporarily disabled. These codes are the same whether SendGrid, Mailgun, Postmark, or your own Postfix box generated the report. They are what you store.

Most ESPs surface the raw response somewhere. SendGrid puts it in the reason field of the Event Webhook. Mailgun puts it in delivery-status.message and delivery-status.code. Amazon SES exposes it through bounce.bouncedRecipients[].diagnosticCode. Grab that string, parse out the 3-digit code and the enhanced status code, and write both to your own bounce table. The ESP's event: "bounced" is a hint, not a record. For the protocol rules these codes follow, see RFC 5321 section 4.2. We covered which RFC governs what in the important RFCs for email.

Define hard versus soft in one place

Write a single classifier that takes the SMTP code + enhanced status + diagnostic text and returns one of: hard, soft, block, unknown. Use that classifier for every ESP. A starting rule set:

  • 5.1.1, 5.1.2, 5.1.10 -> hard (mailbox or domain does not exist)
  • 5.2.1 -> hard (mailbox disabled)
  • 5.2.2 -> soft (mailbox full, retry later, suppress after 3 fails)
  • 5.7.1, 5.7.x -> block (policy rejection, not a dead address, do not auto-suppress)
  • 4.x.x -> soft (transient, retry per ESP backoff)
  • 550 with no enhanced code -> read the diagnostic string, look for "user unknown", "no such user", "address rejected"

One classifier, one truth table, one bounce status per address. That is what makes the data comparable across providers. The discipline of writing the rules down once is the same discipline behind following voluntary standards in the first place.

Sync suppression globally, not per ESP

If an address hard-bounces on SendGrid, it is dead everywhere. Same mailbox, same DNS, same MX. But each ESP keeps its own suppression list and they do not talk to each other. Send the same broadcast through Mailgun a week later and you mail the dead address again, take another hard bounce, and burn reputation on a second IP pool.

The fix is a central suppression table you own, keyed on the normalized email address (lowercase, strip plus-addressing if your policy says so). Every ESP webhook writes into it. Every send job checks it before handing the address to any ESP. If you use multiple sending platforms (transactional on one, marketing on another), this is non-negotiable.

Handle the edge cases that wreck your numbers

  • Block bounces (5.7.x) are not dead addresses. Gmail returning 5.7.1 The IP address sending this message has been temporarily rate limited is about your IP, not the recipient. Suppressing the address is wrong. Pause the send, fix the reputation issue, retry.
  • Backscatter and forged bounces. A bounce arriving at your return path for a message you did not send is spam. Verify the bounce came from a message your platform actually sent (DSN should reference your message ID per RFC 3464) before acting on it.
  • Asynchronous bounces from Microsoft. Outlook and Exchange Online often accept at SMTP time and bounce later via NDR. Your ESP may not link the NDR back to the original send. Match on Message-ID.
  • Soft bounce thresholds. Three consecutive soft bounces on the same address inside 14 days is the threshold most teams settle on for upgrading to hard. Pick a number and apply it everywhere.

Get the classifier, the central suppression list, and the SMTP-code parsing right and your bounce rate becomes a real metric instead of a per-ESP narrative.

Contributors

Who worked on this answer

Every name links to their profile. Every company links to their site. Real people, real accountability.

Ask an AI · tailored to your setup

Sync bounce tracking across multiple ESPs for reliable data

We use ESP name for main sends and sometimes second ESP for specific campaigns, and I'm noticing our bounce reports don't match between them. One system calls something a hard bounce that the other treats as soft. How do I standardize bounce tracking so I actually know how many real unsubscribes I have? Should we reclassify everything to match, or is there a better approach?

Edit the yellow boxes, then send to the AI of your choice.