How delayed email bounces work: Return-Path, DSNs, and webhook correlation

Published on June 01, 2026

A practical guide to delayed email bounces, why the SMTP envelope matters, how Return-Path domains route DSNs, and how Postscale turns them into bounce webhooks.

Most product teams know what a bounce webhook is. Fewer know why some bounces arrive immediately, some arrive minutes later, and some never reach the sending platform at all.

The difference is the SMTP envelope. If you send transactional mail and your app updates customer state from bounce events, the Return-Path is not trivia. It is the address where delayed delivery failures come back.

Immediate vs delayed bounces

There are two common failure paths.

Immediate SMTP rejection happens during delivery. Your mail server connects to the recipient's MX, issues RCPT TO, and the receiver says no:

RCPT TO:<missing@example.net>
550 5.1.1 user unknown

That result is available right away. The sending platform can mark the message as bounced from the delivery log.

Delayed DSN bounce happens after the recipient MX first accepts the message. Later, a downstream system decides delivery failed: the mailbox does not exist, the account is over quota, content policy rejected it, or the message expired in a queue. The receiver then sends a delivery status notification, usually from MAILER-DAEMON, back to the original SMTP envelope sender.

That second path only works if the envelope sender points somewhere you control.

Return-Path is the envelope, not the From header

Email has at least two sender identities:

  • From: is the visible author in the message headers.
  • MAIL FROM is the SMTP envelope sender used for bounces and SPF.

The final Return-Path: header you see in a received message is normally added by the receiving mail server from MAIL FROM. You should think of Return-Path as "where bounces go", not "what the recipient sees as the sender".

For a normal transactional message, the identities can look like this:

Header From: no-reply@yourapp.com
SMTP MAIL FROM: b-7k3n4j2q9p@ps-bounces.yourapp.com

The customer still sees no-reply@yourapp.com. If a delayed bounce happens, the DSN comes back to b-7k3n4j2q9p@ps-bounces.yourapp.com.

Why use a tokenized bounce address?

A delayed DSN is just another email. It may include the original message ID, the failed recipient, a status like 5.1.1, and a diagnostic from the recipient MX. But DSNs are not perfectly consistent across providers.

Embedding an opaque token in the bounce address gives the sending platform a strong correlation key:

b-<token>@ps-bounces.yourapp.com

When the DSN arrives, the platform does not have to guess which send caused it. It extracts <token>, finds the original outbound email row, parses the DSN, and applies the result to the correct message and recipient.

The DNS pattern

For an outbound sending domain, Postscale shows a return-path CNAME:

ps-bounces.yourapp.com. CNAME bounces.postscale.io.

This is a subdomain used only for bounce handling. It does not replace your root MX records and it does not make Postscale receive normal mailbox traffic for yourapp.com.

That distinction matters for common setups:

SetupRoot MXReturn-path CNAME
Google Workspace handles employee mail, Postscale sends app mailKeep Google MXAdd ps-bounces.yourapp.com
Microsoft 365 handles support mail, Postscale sends receiptsKeep Microsoft MXAdd ps-bounces.yourapp.com
Postscale sends and receives all mail for the domainPoint MX to PostscaleAdd ps-bounces.yourapp.com

The return-path CNAME gives delayed bounces a controlled route back without changing where normal inbound mail goes.

End-to-end flow

Here is the full path when a delayed bounce happens:

1. Your app sends a receipt from no-reply@yourapp.com
2. Postscale stores the outbound email and generates a return-path token
3. Postscale sends with MAIL FROM b-<token>@ps-bounces.yourapp.com
4. The recipient MX accepts the message
5. A downstream mailbox or policy check later fails delivery
6. The receiver sends a DSN to b-<token>@ps-bounces.yourapp.com
7. DNS routes ps-bounces.yourapp.com through bounces.postscale.io
8. Postscale parses the DSN and looks up the original message by token
9. The message becomes bounced or deferred
10. Webhooks, stats, and suppression handling run from that parsed event

For hard bounces, Postscale can also add the recipient to the suppression list so future sends fail fast instead of damaging sender reputation.

What the webhook looks like

Your application should treat bounce webhooks as state changes on the outbound message and recipient:

{
  "event": "email.bounced",
  "timestamp": "2026-06-01T10:30:00Z",
  "email_id": "5f7a...",
  "message_id": "a3c6e2b4-...@yourapp.com",
  "from": "no-reply@yourapp.com",
  "recipient": "missing@example.net",
  "bounce_type": "hard",
  "bounce_code": "550",
  "diag_code": "5.1.1",
  "diag_message": "smtp; 550 5.1.1 user unknown"
}

Use message_id or email_id to find your internal notification, then update the recipient state. For example:

  • Mark a signup email as undeliverable.
  • Stop sending invoice reminders to a bad address.
  • Ask the account owner to update billing contact details.
  • Suppress the address in your own CRM or user table.
  • Alert support when a high-value transactional message bounced.

Do not rely only on human-readable diagnostics. Keep them for operators, but use bounce_type, bounce_code, and diag_code for automation.

Hard bounce or soft bounce?

The first digit of the enhanced status code usually tells you what to do:

Status classMeaningProduct behavior
5.x.xPermanent failureStop sending to that address until corrected
4.x.xTemporary failureRetry later and monitor repeated failures
Missing or vagueProvider-specific diagnosticKeep the raw diagnostic for review

Common permanent examples:

  • 5.1.1 user unknown
  • 5.2.1 mailbox disabled
  • 5.7.1 policy rejection

Common temporary examples:

  • 4.2.2 mailbox full
  • 4.4.1 connection timed out
  • 4.7.0 temporarily deferred or rate limited

In practice, receivers are not perfectly clean. A DSN can include a 550 SMTP reply with an enhanced code, or it can include only a text diagnostic. A good pipeline preserves all three pieces: SMTP status, enhanced status, and raw diagnostic.

Use case: billing email that cannot disappear

Suppose you send invoice emails from billing@yourapp.com.

An immediate rejection is easy: the recipient MX says no during delivery and the message is marked bounced within seconds. A delayed bounce is subtler. The MX accepts the invoice, then a mailbox rule or downstream server rejects it later. Without a working Return-Path, your application may keep showing "sent" even though the customer never received the invoice.

With return-path DSN correlation:

  1. The outbound email is tied to a unique bounce token.
  2. The delayed DSN returns to Postscale.
  3. Your webhook handler marks the invoice notification as failed.
  4. Your app can show an account banner, notify finance, or switch to another contact method.

That is the difference between "we sent it" and "we know whether it made it".

Use case: migrating from another provider

During a SendGrid, Mailgun, Postmark, Resend, or SES migration, bounce handling is one of the places teams miss hidden coupling.

Before cutover, check:

  • Do you update user status from bounce webhooks?
  • Do you suppress hard bounces locally as well as in the provider?
  • Do you rely on provider-specific bounce categories?
  • Do you keep the old provider active long enough for delayed DSNs to drain?
  • Have you added the new return-path DNS record before sending production mail?

If you dual-send or gradually shift traffic, normalize old-provider and Postscale bounce events into one internal event shape before updating your database. Your product should not care which provider observed the bounce.

Testing delayed bounces

Do not test only with a nonexistent address. That usually exercises immediate SMTP rejection, not the delayed DSN path.

A better test plan:

  1. Send a real canary through your production sending domain.
  2. Confirm the outbound row has a tokenized envelope sender.
  3. Submit or trigger a synthetic DSN to b-<token>@ps-bounces.yourapp.com.
  4. Confirm the outbound event source is recorded as a delayed DSN.
  5. Confirm your webhook handler updates the right internal record.
  6. Replay the same DSN and confirm your handler is idempotent.

Postscale's Outbound Service guide covers the DNS setup, and the Webhooks guide covers signed webhook verification and duplicate handling.

What can still go wrong

  • The return-path CNAME is missing. Immediate bounces still work, but delayed DSNs cannot reliably reach the sending platform.
  • The CNAME points at the wrong target. The DSN may be accepted by a mailbox provider that has no idea how to correlate it.
  • Root MX was changed by mistake. Outbound-only domains should keep their normal mailbox MX records.
  • The webhook handler is not idempotent. Providers retry webhooks, and DSNs can be duplicated.
  • The app treats all bounces as permanent. Temporary 4.x.x deferrals should not unsubscribe or suppress a customer by themselves.

The short version

If your application depends on reliable delivery state, set up the return-path CNAME before production traffic.

Immediate bounces come from delivery logs. Delayed bounces come back as DSN emails to the SMTP envelope sender. A tokenized Return-Path lets Postscale route those DSNs, parse them, correlate them to the original outbound message, and emit the same email.bounced or email.deferred webhook shape your application already handles.

Further reading: