ESP response codes mapped to internal status model
Why it matters
When a worker catches all ESP errors with a single catch block and re-throws them into the retry pipeline, invalid email addresses are retried five times before being discarded, burning send quota and generating five bounce events against your domain reputation. SendGrid and Mailgun rate response codes differently: a 400 (invalid address) is permanent; a 503 (service unavailable) is transient; a 429 (rate limit) needs a long backoff. Treating all three identically is the mechanism behind CWE-390 (detection of errors without action) and CWE-400 (resource exhaustion from unnecessary retry).
Severity rationale
Medium because misclassified errors waste send quota and degrade sender reputation rather than causing immediate data loss or security breach.
Remediation
Create a classifyEspError function in lib/email/errors.ts and use it in the worker to route each failure class to the correct outcome:
export type EmailFailureKind = 'permanent' | 'transient' | 'rate_limited' | 'quota_exceeded'
export function classifyEspError(err: unknown): EmailFailureKind {
const code = (err as { code?: number; statusCode?: number }).statusCode
if (code === 429) return 'rate_limited'
if (code && code >= 400 && code < 500) return 'permanent'
if (code && code >= 500) return 'transient'
return 'transient'
}
In the job processor, return (acknowledge without retry) on permanent and re-throw on transient so BullMQ applies its backoff schedule.
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the Sending Pipeline & Infrastructure Audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- cwe · CWE-390 — Detection of Error Condition Without Action
- iso-25010:2011 · reliability.fault-tolerance — Fault Tolerance
- cwe · CWE-400 — Uncontrolled Resource Consumption
Taxons
History
- 2026-04-18·v1.0.0·Initial import from sending-pipeline-infrastructure·automated