---
title: Bounce Handling
path: concepts/bounce-handling
status: published
---

# Bounce Handling

A bounce is a delivery failure. ScaiSend classifies bounces, records them on the message timeline, fans out `bounce` events to webhooks, and adds the recipient to the suppression list so you don't repeat the failure. This page explains how.

## Two paths to a bounce

Bounces arrive two ways:

1. **Synchronous SMTP response.** The recipient MX returns a 5xx at the end of the SMTP conversation. ScaiSend sees it immediately.
2. **Asynchronous DSN.** The recipient MX accepts the message (returns 2xx), but later — after internal routing — can't deliver. It sends a Delivery Status Notification ([RFC 3464](https://datatracker.ietf.org/doc/html/rfc3464)) back to the envelope sender. ScaiSend's inbound SMTP server receives and parses this.

Both paths end up the same: a `bounce` event on the message, the address added to the bounce suppression list.

## Synchronous bounces

At the end of `RCPT TO:` or `DATA`, the recipient can reply with:

| Response | Classification | Action |
|----------|----------------|--------|
| `250 OK` | Delivered | Record `delivered` event, update message status |
| `4xx` | Temporary failure (deferred) | Retry with exponential backoff; record `deferred` event |
| `5xx` | Permanent failure (bounce) | Record `bounce` event; add to bounce suppression list |

ScaiSend parses the response string to classify further. A `550 5.1.1 User unknown` is a hard bounce (recipient doesn't exist). A `552 5.2.2 Mailbox full` is a soft bounce (might succeed later if retried). A `550 5.7.1 Message rejected due to content` is a block (policy decision; not necessarily permanent).

### Bounce types recorded

| `bounce_type` | Trigger |
|---------------|---------|
| `hard` | Permanent failure: invalid recipient, non-existent mailbox, account closed |
| `soft` | Temporary failure that exhausted retries: mailbox full, message too large |
| `block` | Sender reputation issue: IP blocklisted, content rejected |

## Asynchronous bounces (DSNs)

Sometimes the recipient MX accepts a message, queues it internally, and only later discovers it can't deliver (e.g., forward loop, downstream server unreachable, virus-scan rejection). The convention is to send a DSN back to the envelope sender.

ScaiSend's inbound SMTP server (port 25) accepts these DSNs. The parser:

1. Reads the multi-part message body.
2. Extracts the `message/delivery-status` part, which contains machine-readable reporting.
3. Finds the `Original-Recipient`, `Action` (e.g., "failed"), `Status` (SMTP enhanced status code), and `Diagnostic-Code` fields.
4. Matches the DSN to the original message via the `Message-ID` or the `X-ScaiSend-Message-Id` header that ScaiSend injected.
5. Records a `bounce` event with parsed fields and adds the recipient to the suppression list.

### What the inbound server accepts

The inbound server is narrowly scoped: it accepts DSN messages, ARF feedback-loop reports, and refuses everything else. It's not a general-purpose MTA. Arbitrary inbound mail from the internet is rejected at the SMTP conversation level.

For the inbound server to work, port 25 must be reachable and an `A`/`AAAA` record pointing to it. See [Deployment](../troubleshooting/deployment).

## Retry policy for deferrals

A `4xx` response doesn't give up immediately. ScaiSend retries with exponential backoff:

| Attempt | Delay after previous attempt |
|---------|------------------------------|
| 1 (initial send) | — |
| 2 | ~60 seconds |
| 3 | ~5 minutes |
| 4 | ~15 minutes |
| 5 | ~1 hour |
| 6 (last) | ~4 hours |

Actual delays have jitter to avoid thundering herds, and are bounded by `SMTP_MAX_RETRIES` (default 5 retries = 6 attempts total) and `SMTP_RETRY_BASE_DELAY` (default 60 seconds).

After the final retry, if still deferred, ScaiSend classifies as a soft bounce and marks the message `BOUNCED`.

### Greylisting

Some recipient MXs greylist — they return `4xx` on first attempt from an unknown sender, expecting a real MTA to retry. ScaiSend detects the common greylisting responses (e.g., `4.7.1` with specific wording like "greylisted") and retries more aggressively (shorter initial delay), which satisfies the greylist check.

You typically see one `deferred` event, then a `delivered` event seconds later. Nothing to do; this is working as intended.

## Suppression

Every hard bounce automatically adds the recipient to the tenant's bounce suppression list. Future sends to that address are dropped (status `FAILED`, event `dropped`, reason `suppressed_bounce`) unless you bypass with `mail_settings.bypass_bounce_management: {enable: true}`.

Soft bounces also add to the list by default. If you'd rather let soft-bounced addresses be retried on future sends, delete them from the list after they resolve:

```bash
curl -X DELETE https://scaisend.scailabs.ai/v3/suppression/bounces/user@example.com \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

See [Suppressions](suppressions) for the full suppression model.

## Event payload

A `bounce` event:

```json
{
  "event_id": "evt_01HXYZ",
  "event_type": "bounce",
  "timestamp": 1713888000,
  "message_id": "msg_01ABC",
  "recipient_email": "invalid@example.com",
  "tenant_id": "tnt_acme",
  "bounce_type": "hard",
  "bounce_reason": "User unknown",
  "smtp_response": "550 5.1.1 User unknown",
  "metadata": {
    "diagnostic_code": "smtp; 550 5.1.1 <invalid@example.com> User unknown in virtual mailbox table",
    "reporting_mta": "dns; mx1.example.com"
  }
}
```

`metadata.diagnostic_code` is what the recipient MX actually said (for synchronous bounces, it's the raw SMTP response; for async DSNs, it's the `Diagnostic-Code` field from the DSN). Log this for support triage.

## Interpreting bounce reasons

Common bounce reasons and their meaning:

| SMTP class | Typical cause | What to do |
|------------|---------------|------------|
| `5.1.1` | Recipient doesn't exist | Remove from your list; the address is dead |
| `5.1.10` | Recipient address null-routed | Same as above |
| `5.2.1` | Mailbox disabled | User left the company or was deactivated |
| `5.2.2` | Mailbox full | Soft bounce; retry later possibly |
| `5.4.4` | Unable to route | DNS issue at recipient; usually transient |
| `5.5.0` | Syntax error in message | Check your message structure; template bug |
| `5.7.1` | Message rejected for policy | Spam filter, IP reputation, content block |
| `5.7.26` | DMARC failure | Your DKIM/SPF/DMARC isn't aligned — fix DNS |

## Reputation management

Hard bounce rate is the single most important deliverability metric. Over 2% and you'll start getting rate-limited by major providers. Over 5% and you'll be outright blocked by some.

Monitor:

```bash
curl "https://scaisend.scailabs.ai/v3/stats?start_date=2026-04-01&end_date=2026-04-23" \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

Calculate `bounces / requests`. If it's trending up:

- Audit your list sources. Did you import addresses from a suspect list?
- Audit your signup flow. Are you validating emails at registration?
- Check a random sample of bounced addresses. If they're all `5.1.1`, your list has invalid addresses. If they're `5.7.*`, your reputation is the problem.

## Related

- [Suppressions](suppressions) — what happens to bounced addresses.
- [Feedback Loops](feedback-loops) — the spam-complaint counterpart to bounces.
- [Events and Webhooks](events-and-webhooks) — receiving `bounce` events.
