Email bounces are inevitable in production environments. The difference between competent and negligent email infrastructure is how you handle them. SMTP error codes tell you exactly what went wrong, but only if you know how to read them and what action each one demands. This guide covers production patterns that work and the anti-patterns that waste your time.
Understanding SMTP Response Codes
SMTP uses three-digit response codes where the first digit determines severity. The structure is deliberate and consistent across all compliant mail servers.
The First Digit: Status Class
- 2xx: Success. The message was accepted.
- 4xx: Temporary failure. Retry later.
- 5xx: Permanent failure. Do not retry.
The critical distinction is 4xx versus 5xx. Treat them differently or you will either hammer servers with pointless retries or lose legitimate mail.
The Second Digit: Category
The second digit narrows the problem domain:
- x0x: Syntax errors
- x1x: Informational responses
- x2x: Connection status
- x5x: Mail system status
The third digit provides specifics, but the first two digits give you the pattern.
Critical Production Codes and What They Demand
421: Service Not Available
What it means: The receiving server is overloaded, performing maintenance, or rate-limiting your connection.
Best practice: Implement exponential backoff with jitter. Start with a 15-minute retry, then 30, then 60. Do not retry immediately.
Anti-pattern: Setting a fixed 5-minute retry interval. You will hammer the server during its maintenance window and get blocklisted.
# Good: Exponential backoff with jitter
import random
import time
def retry_with_backoff(attempt, base_delay=900):
delay = base_delay * (2 ** attempt)
jitter = random.uniform(0, delay * 0.1)
return min(delay + jitter, 3600)
450: Mailbox Unavailable (Temporary)
What it means: The mailbox exists but cannot accept mail right now. Common causes include quota exceeded or mailbox locked during maintenance.
Best practice: Retry for up to 48 hours with increasing intervals. Alert the sender after 24 hours that delivery is delayed.
Anti-pattern: Treating 450 like 550 and bouncing immediately. You will lose legitimate mail when users are temporarily over quota.
451: Local Error in Processing
What it means: The server encountered an internal error unrelated to your message content. Often indicates disk full, database timeout, or misconfiguration.
Best practice: Retry normally but log these aggressively. If you see 451 from the same domain repeatedly, contact their postmaster.
Anti-pattern: Ignoring 451 codes in your monitoring. They signal operational problems that need human attention.
452: Insufficient System Storage
What it means: The server has run out of disk space or queue capacity.
Best practice: Retry with extended intervals. These usually resolve within hours as the admin clears space.
Anti-pattern: Routing around the problem by trying a backup MX. The backup MX likely shares the same storage and will return the same error.
550: Mailbox Not Found
What it means: The recipient address does not exist. This is permanent.
Best practice: Stop retrying immediately. Remove the address from your mailing lists. Update your user database if this is a customer email.
Anti-pattern: Retrying 550 errors hoping the mailbox will be created. It won't. You are wasting resources and damaging your sender reputation.
# Good: Parse and categorize bounces
grep "550" /var/log/exim4/mainlog | \
awk '{print $NF}' | \
sort | uniq -c | \
sort -rn | head -20
551: User Not Local
What it means: The mailbox exists elsewhere. Often returned with a forwarding hint.
Best practice: Check for a forwarding address in the bounce message. Update your records if provided.
Anti-pattern: Treating 551 like 550. The user exists, just not where you think.
552: Mailbox Full
What it means: The recipient's mailbox has exceeded its quota.
Best practice: Retry for 5-7 days. Alert the sender after 24 hours. Many users check email sporadically and will clear space.
Anti-pattern: Bouncing immediately. Mailbox full is temporary for active users.
553: Mailbox Name Not Allowed
What it means: The address format is invalid or violates the server's policy.
Best practice: Do not retry. Log these for analysis. You likely have a validation bug in your input handling.
Anti-pattern: Accepting email addresses that fail RFC validation. Validate before accepting, not after bouncing.
// Good: Validate before accepting
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
// Additional check for RFC-compliant format
if (!preg_match('/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $email)) {
return false;
}
554: Transaction Failed
What it means: Generic permanent failure. Often accompanied by an explanation.
Best practice: Parse the extended message text. 554 can mean policy rejection, spam filtering, or authentication failure. The explanation matters.
Anti-pattern: Treating all 554 codes identically. A 554 for SPF failure needs different action than a 554 for content filtering.
Enhanced Status Codes: The X.Y.Z Format
Modern mail servers return enhanced status codes in X.Y.Z format alongside the three-digit code. These provide machine-readable detail.
Structure
- X: Class (2=success, 4=temporary, 5=permanent)
- Y: Subject (0=other, 1=addressing, 2=mailbox, 4=network, 5=protocol, 7=security)
- Z: Detail
Key Enhanced Codes
5.1.1: Bad destination mailbox address. The address does not exist.
5.7.1: Delivery not authorized. Often means your IP is blocklisted or your domain failed authentication.
4.7.1: Temporary authentication failure. Often means greylisting. Retry after 15+ minutes.
5.7.26: Authentication required. The recipient requires SPF, DKIM, or DMARC validation.
Best practice: Log enhanced codes separately and trend them. A sudden increase in 5.7.1 codes means you have a reputation problem.
# Extract enhanced status codes from Postfix logs
grep "status=bounced" /var/log/mail.log | \
grep -oP '\(\K[0-9]\.[0-9]\.[0-9]+' | \
sort | uniq -c | sort -rn
Production Monitoring Patterns
What to Monitor
-
Bounce rate by code category: Track 4xx versus 5xx separately. Rising 4xx suggests external problems. Rising 5xx suggests configuration or reputation issues.
-
Bounce rate by recipient domain: One domain bouncing everything suggests blocklisting or authentication failure with that provider.
-
Time-to-bounce: Immediate bounces suggest syntax or authentication problems. Delayed bounces suggest delivery queue issues.
-
Enhanced code distribution: Track 5.7.1 (authorization) and 5.7.26 (authentication) separately. These need immediate action.
Alert Thresholds
Warning: Bounce rate exceeds 5% over a 1-hour window.
Critical: Bounce rate exceeds 10% over 15 minutes, or any single recipient domain shows >50% bounce rate.
Anti-pattern: Alerting on individual bounces. You will drown in noise. Alert on patterns.
# Example Prometheus alert rule
groups:
- name: email_delivery
rules:
- alert: HighBounceRate
expr: |
rate(smtp_bounces_total[1h]) / rate(smtp_sent_total[1h]) > 0.05
for: 15m
labels:
severity: warning
annotations:
summary: "Email bounce rate above 5%"
- alert: AuthenticationFailures
expr: |
increase(smtp_bounces_total{enhanced_code=~"5\.7\.(1|26)"}[15m]) > 10
labels:
severity: critical
annotations:
summary: "Authentication-related bounces detected"
Handling Bounces in Application Code
Parse the Full Bounce Message
Bounce messages contain structured data. Parse it instead of relying solely on the SMTP code.
Look for: - Original recipient address - Original message ID - Remote MTA hostname - Full SMTP transaction log - Enhanced status code - Human-readable explanation
Anti-pattern: Extracting only the SMTP code and discarding context. You need the full picture for debugging.
Categorize and Route
# Good: Structured bounce handling
class BounceHandler:
def categorize(self, smtp_code, enhanced_code):
if smtp_code.startswith('5'):
if enhanced_code in ['5.1.1', '5.1.2']:
return 'HARD_BOUNCE_ADDRESS'
elif enhanced_code.startswith('5.7'):
return 'HARD_BOUNCE_POLICY'
else:
return 'HARD_BOUNCE_OTHER'
elif smtp_code.startswith('4'):
if enhanced_code == '4.7.1':
return 'SOFT_BOUNCE_GREYLIST'
elif smtp_code in ['450', '452']:
return 'SOFT_BOUNCE_RESOURCE'
else:
return 'SOFT_BOUNCE_OTHER'
def handle(self, bounce_category, recipient, original_msg_id):
if bounce_category.startswith('HARD_BOUNCE'):
self.mark_invalid(recipient)
self.notify_sender(original_msg_id, bounce_category)
elif bounce_category == 'SOFT_BOUNCE_GREYLIST':
self.schedule_retry(original_msg_id, delay=1800)
elif bounce_category.startswith('SOFT_BOUNCE'):
self.schedule_retry(original_msg_id, exponential=True)
Update Your Address Database
When you receive a hard bounce, update your records immediately. Do not send to that address again until the user re-verifies it.
Anti-pattern: Continuing to send to hard-bounced addresses. This is how you get blocklisted.
Anti-Patterns That Damage Reputation
Retrying Permanent Failures
Retrying 5xx codes damages your sender reputation with every attempt. Receiving servers track this.
Ignoring Greylisting
Greylisting returns 4.7.1 or 451 and expects you to retry after 15+ minutes. Retrying immediately or giving up marks you as a spammer.
Not Implementing VERP
Variable Envelope Return Path lets you track which recipient address caused a bounce. Without it, you cannot automatically clean your lists.
# Good: VERP in Postfix
# main.cf
default_verp_delimiters = +=
Sending from Residential or Dynamic IP Ranges
Most MTAs reject or heavily filter mail from dynamic IP ranges. Use a proper MTA with a static IP and PTR record.
Missing or Broken SPF, DKIM, and DMARC
Authentication failures generate 5.7.x bounces. These are permanent configuration problems, not temporary glitches.
Debugging Checklist
When investigating delivery problems:
- Check the full SMTP code and enhanced status code
- Verify SPF, DKIM, and DMARC records are published and valid
- Confirm your sending IP has a PTR record
- Check if your IP or domain appears on public blocklists
- Review the full SMTP transaction log from your MTA
- Test with a different recipient at the same domain to rule out mailbox-specific issues
- Check recipient domain MX records and try a manual SMTP connection
# Manual SMTP test
telnet mx.example.com 25
HELO yourdomain.com
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
QUIT
Conclusion
Reading SMTP bounce codes correctly is not optional in production. Treat 4xx and 5xx codes differently, implement proper retry logic with exponential backoff, and monitor bounce patterns rather than individual failures. Parse enhanced status codes for machine-readable detail, update your address database immediately on hard bounces, and never retry permanent failures. Your sender reputation depends on handling bounces correctly, and your deliverability depends on your reputation. The patterns in this guide are how professional email infrastructure operates in 2026.
