Soft bounce threshold before suppression
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:
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:
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:2011 · functional-correctness — Functional Correctness (functional suitability)
Taxons
History
- 2026-04-18·v1.0.0·Initial import from data-quality-list-hygiene·automated