Hard bounce vs soft bounce: what they mean and how to handle each
Updated
A bounce is a rejection message from the receiving side. Whether it is hard or soft decides whether to keep sending.
The difference
| | Hard bounce | Soft bounce | |---|---|---| | Meaning | Permanent failure | Temporary failure | | SMTP reply class | 5xx | 4xx | | Typical causes | Address does not exist, domain does not exist, recipient blocked you | Mailbox full, server busy, message too large, temporary policy block | | Right response | Stop sending to this address | Retry later, then give up if it persists | | Reputation impact | High if you keep sending | Lower, but repeated retries add up |
Reading the codes
The first digit gives the class (4 temporary, 5 permanent). Enhanced status codes add detail. Examples you will meet:
5.1.1mailbox does not exist: a hard bounce. Remove the address.5.1.2domain does not exist: a hard bounce, usually a typo.4.2.2or5.2.2mailbox full: soft in practice, since people empty mailboxes.5.7.1rejected for policy or spam reasons: look at your authentication and content, not the address.4.7.xtemporary deferral, often greylisting: retry.
A 5.7.1 is not a bad address. If you suppress every 5xx you will lose real customers because your own authentication was misconfigured.
What Inboxili does
When a message bounces, Inboxili records the bounce on that send. Permanent bounces and spam complaints add the address to your suppression list automatically, and suppressed addresses are skipped in campaigns. Transient and undetermined bounces are recorded for your analytics but do not suppress the address.
It also posts a bounced webhook with bounce_type (permanent, transient or undetermined), bounce_subtype and the diagnostic_code, so your own system can react.
A webhook handler (Node.js)
import express from "express";
import { verifyWebhookSignature } from "@inboxili/node";
const app = express();
app.post("/webhooks/inboxili", express.raw({ type: "application/json" }), async (req, res) => {
if (!verifyWebhookSignature(req.body, req.get("X-Inboxili-Signature") ?? "", process.env.INBOXILI_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (req.get("X-Inboxili-Event") === "bounced") {
if (event.bounce_type === "permanent") {
await db.users.update({ email: event.email }, { emailDeliverable: false });
} else {
await db.users.increment({ email: event.email }, "softBounces");
// After several soft bounces in a row, treat the address as unreachable.
}
}
res.sendStatus(200);
});
Why this split: a permanent bounce is decisive, so mark the address undeliverable and tell the user in your product ("we could not reach you at this address"). A soft bounce is information, so count it.
Practical rules
- Never keep mailing a hard-bounced address. It is the fastest way to damage your reputation.
- Do not suppress on policy rejections without looking at why you were rejected.
- Fix typos at signup. Ask users to confirm their address, and verify it with a link.
- Warn the user in-app when their email is undeliverable. A bounced password reset is a support ticket waiting to happen.
- Watch the rate. A sudden rise in bounces usually means a bug or a bad list import. Check with the analytics before sending more.
- Removing a suppression is deliberate. Only do it when the cause is fixed, for example the recipient confirms their mailbox works.
Frequently asked questions
- How many soft bounces before I stop sending?
- There is no universal number. A common rule is to stop after several consecutive soft bounces over a few days, since a mailbox that stays full or unreachable is effectively dead.
- Does Inboxili suppress on every bounce?
- No. Permanent (hard) bounces and complaints suppress the address. Transient and undetermined bounces are recorded but do not suppress it.
Handle bounces with Inboxili
Create a workspace, verify a domain, and make your first API call.