Bounce Handling Without Retrying Forever
Hard bounces must suppress the address. Soft bounces may retry with a cap. Infinite queues hurt IP reputation and hide dead users in your app.
Netbay Cloud Team
Netbay Engineering
On this page
A 250 from your local MTA means the message was queued. It does not mean a mailbox exists. Bounce handling is the difference between a clean sending domain and an IP that inbox providers quietly throttle. The rule is simple: never retry a hard bounce, cap the rest, and tell the application so it stops generating mail for dead addresses.
Hard versus soft versus complaints
- Hard bounce (5xx): the address is invalid, the domain does not exist, or the receiver refused you as policy. Do not retry. Suppress immediately.
- Soft bounce (4xx): mailbox full, greylist, temporary deferral. Retry with backoff. Give up after a bounded window (hours to a couple of days, not weeks).
- Complaint (feedback loop or FBL): a human marked spam. Treat it like a hard bounce even if the SMTP session succeeded. Send to that address again and your complaint rate is the metric that gets you blocked.
DSN (Delivery Status Notification) messages come back to the envelope sender. If MAIL FROM is noreply@example.com and that mailbox is a black hole, you will never see the bounce. Use a dedicated bounce address or VERP (variable envelope return path) so you can map a DSN to the original recipient.
Cap the Postfix queue
Default maximal_queue_lifetime is five days. That is long enough to hammer a dead mailbox through greylisting windows and to look like a retry bot. For transactional mail, shorter is kinder to everyone.
# /etc/postfix/main.cf
maximal_queue_lifetime = 12h
bounce_queue_lifetime = 6h
maximal_backoff_time = 1h
minimal_backoff_time = 15m
queue_run_delay = 15m
bounce_notice_recipient = bounces@example.com
notify_classes = bounce, 2bounce, delay
delay_warning_time = 0delay_warning_time = 0 disables "your mail is delayed" noise to users. 2bounce (double bounce) is what happens when the DSN itself cannot be delivered; those must land in a mailbox a human reads, or you will leak queue disk and learn nothing.
Soft bounce retries should be geometric, not aggressive. Fifteen minutes, then an hour, then stop at twelve hours is plenty for a password reset that the user needed now.
Parse DSNs or, better, consume webhooks
If you send through an ESP, do not parse bounce mail by hand. Subscribe to SES SNS, Mailgun event webhooks, or Postmark's bounce API. Classify with their taxonomy (HardBounce, SoftBounce, SpamComplaint) and write a suppression row keyed by email.
function applyEvent(db, ev) {
const email = String(ev.recipient || "").toLowerCase();
if (!email) return;
if (ev.type === "HardBounce" || ev.type === "SpamComplaint") {
db.run(
"INSERT OR REPLACE INTO mail_suppress (email, reason, ts) VALUES (?, ?, ?)",
[email, ev.type, Date.now()]
);
return;
}
if (ev.type === "SoftBounce" && ev.consecutive >= 3) {
db.run(
"INSERT OR REPLACE INTO mail_suppress (email, reason, ts) VALUES (?, ?, ?)",
[email, "SoftBounceCap", Date.now()]
);
}
}Check that table before every send. A suppressed address is not a retry candidate. Honour list-unsubscribe headers on bulk mail so complaints never start.
If you send direct, you will parse DSN MIME (message/delivery-status) from a bounce mailbox. Match the original Message-ID. VERP (bounces+user=ex.com@example.com as envelope sender) makes the mapping mechanical. Without VERP you guess, and guessing re-sends to the same dead address.
What infinite retry actually costs
Inbox providers watch unknown-user rates. A list that is 8 percent dead looks like a harvested database. Continuing to retry 550 5.1.1 is how a previously clean IP joins a blocklist. The application should also mark the account email as unverified and stop the job that generates the mail. Queue hygiene is a product feature.
RFC 3463 status codes are more stable than the English text around them. 5.1.1 is unknown user. 5.1.2 is unknown domain. 5.7.1 is policy, which may be your IP, not the recipient. Do not suppress an address because one Gmail MX said 5.7.1 Access denied; that is often the sending IP, and retrying later after you fix PTR can succeed. Do suppress on 5.1.1 from the recipient's own MX. Keep a reason column so you can tell those two apart six months later.
A warmup schedule without suppression is wasted. New domains that blast a stale imported list burn the IP before the second day. Validate addresses at signup, send a confirm link, and only then consider the address live.
Takeaway
Hard bounces and complaints suppress; soft bounces retry with a short lifetime; the app must know. You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and keep bounce_notice_recipient on a mailbox you actually read — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance