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.
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.
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.
ID: data-quality-list-hygiene.suppression-bounce.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')
}
}