# Soft bounce threshold before suppression

- **Pattern:** `ab-000871` (`data-quality-list-hygiene.suppression-bounce.soft-bounce-threshold`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000871
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000871)

## Why it matters

Soft bounces are temporary delivery failures (mailbox full, server temporarily unavailable) that resolve on their own in most cases. Suppressing on the first soft bounce is over-aggressive and discards valid contacts experiencing transient issues. Ignoring soft bounces entirely is the opposite failure: repeated soft bounces to the same address indicate a systemic problem — the mailbox may have been abandoned, full indefinitely, or the server may be filtering your sender. Without a counter and threshold, soft-bouncing addresses accumulate undetected on the active list, raising your overall bounce rate and generating misleading delivery stats.

## Severity rationale

High because absent soft-bounce thresholding, both over-suppression (instant) and under-suppression (never) result in list quality degradation — one destroys valid contacts, the other accumulates chronic bouncers.

## Remediation

Implement a soft bounce counter with a configurable window and threshold:

```ts
const SOFT_BOUNCE_THRESHOLD = parseInt(process.env.SOFT_BOUNCE_THRESHOLD ?? '3')
const SOFT_BOUNCE_WINDOW_DAYS = parseInt(process.env.SOFT_BOUNCE_WINDOW_DAYS ?? '7')

async function handleSoftBounce(email: string, eventId: string) {
  const windowStart = new Date(Date.now() - SOFT_BOUNCE_WINDOW_DAYS * 86_400_000)
  const recentCount = await db.bounceEvent.count({
    where: { email, bounce_type: 'soft', occurred_at: { gte: windowStart } }
  })
  await db.bounceEvent.create({
    data: { email, bounce_type: 'soft', occurred_at: new Date(), source_event_id: eventId }
  })
  if (recentCount + 1 >= SOFT_BOUNCE_THRESHOLD) {
    await suppressContact(email, 'soft_bounce_threshold')
  }
}
```

Reset the counter on successful delivery to that address. Default threshold of 3 within 7 days matches industry guidance; tune via environment variables rather than hardcoding.

## Detection

- **ID:** `soft-bounce-threshold`
- **Severity:** `high`
- **What to look for:** Count all bounce handling code paths and classify each by bounce type (hard vs soft). Check how soft bounces (temporary failures — mailbox full, server temporarily unavailable) are handled. A single soft bounce should not trigger suppression, but repeated soft bounces within a short window indicate a deliverability problem. Look for a soft bounce counter per email address and a threshold (commonly at least 3 bounces within 7 days) that triggers suppression. Quote the actual threshold values found in the code.
- **Pass criteria:** Soft bounces are counted per email address. After hitting a configurable threshold of at least 3 soft bounces within a rolling window of no more than 30 days, the address is automatically suppressed. The counter resets on successful delivery.
- **Fail criteria:** Soft bounces trigger immediate suppression on first occurrence (too aggressive) or are never counted (ignored entirely). No threshold-based logic exists.
- **Skip (N/A) when:** Your ESP handles soft bounce thresholding natively and the application does not need to implement its own logic.
- **Detail on fail:** Example: `"All bounces treated identically — soft bounces suppress immediately on first occurrence"` or `"Soft bounces logged as events but no counter or threshold implemented — soft bouncing addresses accumulate undetected"`
- **Remediation:** Implement a soft bounce counter with threshold:

  ```ts
  const SOFT_BOUNCE_THRESHOLD = 3
  const SOFT_BOUNCE_WINDOW_DAYS = 7

  async function handleSoftBounce(email: string, eventId: string) {
    const windowStart = new Date(Date.now() - SOFT_BOUNCE_WINDOW_DAYS * 86_400_000)

    const recentBounces = await db.bounceEvent.count({
      where: {
        email,
        bounce_type: 'soft',
        occurred_at: { gte: windowStart }
      }
    })

    await db.bounceEvent.create({
      data: { email, bounce_type: 'soft', occurred_at: new Date(), source_event_id: eventId }
    })

    if (recentBounces + 1 >= SOFT_BOUNCE_THRESHOLD) {
      await suppressContact(email, 'soft_bounce_threshold')
    }
  }
  ```

## External references

- iso-25010 functional-correctness

Taxons: data-integrity, privacy-consent

HTML version: https://auditbuffet.com/patterns/ab-000871
