SMTP 421 and 450 responses are explicit instructions from the receiving mail server to stop sending and retry later — they are not failures to ignore or retry immediately. RFC5321-s4.2.1 specifies that temporary failures must trigger a retry with increasing delay. Retrying at the same rate after a 421 is the sending equivalent of knocking louder when no one answers — it accelerates the point at which the ISP upgrades the temporary block to a permanent one. Systems that treat 4xx responses as hard failures (discarding the message) also lose deliverability: the message is gone, not deferred.
High because immediate retry at unchanged rate after 4xx responses consistently causes escalation from temporary ISP deferral to extended blocking, with message loss occurring when non-retried 4xx responses are incorrectly treated as permanent failures.
Track backoff state per ISP in memory (or Redis for multi-worker setups) and check it before dequeueing. Exponential backoff starting at 5 minutes, capped at 160 minutes, with automatic reset on successful delivery:
const ispBackoff = new Map<string, { factor: number; until: Date }>()
export function handleSmtpError(ispDomain: string, code: number): void {
if (code >= 400 && code < 500) {
const current = ispBackoff.get(ispDomain)
const factor = Math.min((current?.factor ?? 1) * 2, 32)
ispBackoff.set(ispDomain, {
factor,
until: new Date(Date.now() + factor * 5 * 60_000)
})
} else if (code >= 200 && code < 300) {
ispBackoff.delete(ispDomain) // Reset on success
}
}
export function isIspBackedOff(ispDomain: string): boolean {
const b = ispBackoff.get(ispDomain)
if (!b || b.until <= new Date()) { ispBackoff.delete(ispDomain); return false }
return true
}
In your queue worker, skip ISPs that are backed off and re-queue those jobs at the backoff expiry time. This pattern applies only to direct SMTP; ESP API clients (SendGrid HTTP API, etc.) handle retry internally.
ID: deliverability-engineering.sending-throttling.adaptive-throttling
Severity: high
What to look for: Count all SMTP error response handlers and classify each by response code range. Examine how SMTP or ESP error responses are handled. Look for code that detects 421 (service not available / try again later), 450 (requested mail action not taken), or similar 4xx temporary deferral responses. Check if the system backs off (reduces rate or pauses) when receiving these codes, rather than immediately retrying at the same rate or failing permanently.
Pass criteria: At least 1 adaptive backoff mechanism handles 4xx responses — the system reduces sending rate to the affected domain/ISP, waits before retrying, and resumes at reduced rate. The backoff is proportional to the error rate (more errors → longer backoff).
Fail criteria: 4xx responses cause immediate retry at the same rate or are treated as failures without rate adjustment. No backoff logic exists.
Skip (N/A) when: The project sends via an ESP API (not direct SMTP) where the ESP manages retry and backoff internally.
Detail on fail: "4xx SMTP responses retried immediately at same rate — risks triggering further deferrals and eventual IP block" or "No adaptive throttle logic found — sending rate is fixed regardless of deferral signals"
Remediation: Implement adaptive backoff on 4xx responses:
const ispBackoff = new Map<string, { factor: number; until: Date }>()
export async function handleSmtpError(
ispDomain: string,
responseCode: number
): Promise<void> {
if (responseCode >= 400 && responseCode < 500) {
// Temporary failure — back off
const current = ispBackoff.get(ispDomain)
const factor = Math.min((current?.factor ?? 1) * 2, 32) // Max 32x backoff
const backoffMinutes = factor * 5 // Start at 5min, max 160min
ispBackoff.set(ispDomain, {
factor,
until: new Date(Date.now() + backoffMinutes * 60 * 1000)
})
console.warn(
`ISP ${ispDomain} returned ${responseCode} — backing off ${backoffMinutes}min (factor: ${factor}x)`
)
} else if (responseCode >= 200 && responseCode < 300) {
// Successful delivery — reset backoff
ispBackoff.delete(ispDomain)
}
}
export function isIspBackedOff(ispDomain: string): boolean {
const backoff = ispBackoff.get(ispDomain)
if (!backoff) return false
if (backoff.until <= new Date()) {
ispBackoff.delete(ispDomain)
return false
}
return true
}