SendGrid Bounces & Blocks: Handle Undeliverable Emails & Protect Reputation
In this tutorial, you will learn about SendGrid Bounces & Blocks: Handle Undeliverable Emails & Protect Reputation. We cover key concepts, practical examples, and best practices to help you master this topic.
Bounces and blocks are undeliverable emails that damage sender reputation. Understanding their types, handling them automatically, and cleaning your list protects deliverability.
What You'll Learn
The difference between hard bounces, soft bounces, and blocks, how to Process bounce Webhooks, implement automatic bounce handling, clean email lists, and monitor bounce rates.
Why It Matters
Bounce rates above 5% trigger ISP blocks. DodaTech maintains under 1% by automatically removing bounced addresses and monitoring bounce webhooks in real-time.
Real-World Use
An email to alice@example.com bounces (address doesn't exist). SendGrid sends a bounce event Webhook. The app removes the address from the active list and logs it for admin review.
flowchart LR
A["Send Email\nto bob@old.com"] --> B["ISP Rejects\nAddress Invalid"]
B --> C["SendGrid\nBounce Webhook"]
C --> D["App Processes\nEvent"]
D --> E["Remove from\nActive List"]
D --> F["Log Bounce\n+ Timestamp"]
E --> G["Future Sends\nSkip Address"]
style B fill:#fecaca,stroke:#dc2626
style C fill:#dbeafe,stroke:#2563eb
style E fill:#bbf7d0,stroke:#16a34a
Understanding Bounce Types
# Hard Bounce: Permanent failure (address doesn't exist)
# Soft Bounce: Temporary failure (mailbox full, server busy)
# Block: ISP rejected (spam, blacklist, rate limit)
# SendGrid categorizes in webhook events:
# {
# "event": "bounce",
# "email": "alice@example.com",
# "bounce_type": "permanent", # or "transient"
# "reason": "550 5.1.1 The email account that you tried to reach does not exist"
# }
def classify_bounce(event):
bounce_type = event.get("bounce_type")
reason = event.get("reason", "")
if bounce_type == "permanent":
print(f"Hard bounce: {event['email']} — remove from list")
return "remove"
elif bounce_type == "transient":
print(f"Soft bounce: {event['email']} — retry later")
return "retry"
elif bounce_type == "block":
print(f"Block: {event['email']} — investigate reason")
return "investigate"
Processing Bounce Webhooks
from flask import Flask, request, jsonify
app = Flask(__name__)
bounced_emails = set()
@app.route("/sendgrid/bounces", methods=["POST"])
def handle_bounces():
events = request.get_json()
for event in events:
if event["event"] in ("bounce", "block", "dropped"):
email = event["email"]
event_type = event["event"]
reason = event.get("reason", "No reason")
print(f"[{event_type}] {email}: {reason[:100]}")
if event_type == "bounce" and event.get("bounce_type") == "permanent":
bounced_emails.add(email)
remove_from_active_list(email)
log_bounce(email, reason)
return jsonify({"status": "ok"}), 200
def remove_from_active_list(email):
print(f"Removed {email} from active send list")
def log_bounce(email, reason):
print(f"Logged bounce: {email} — {reason[:50]}...")
Managing Bounces via API
def get_bounce_list():
response = sg.client.suppression.bounces.get()
bounces = response.to_dict()
print(f"Total bounces: {len(bounces)}")
for b in bounces[:5]:
print(f" {b['email']} — {b.get('reason', 'N/A')[:60]}")
return bounces
def delete_bounce(email):
response = sg.client.suppression.bounces._(email).delete()
print(f"Removed {email} from bounces: {response.status_code}")
def clear_all_bounces():
response = sg.client.suppression.bounces.delete()
print(f"Cleared all bounces: {response.status_code}")
# Get current bounces
get_bounce_list()
# Expected output:
# Total bounces: 15
# alice@old.com — 550 5.1.1 The email account that you tried to reach does not exist
# bob@invalid.com — 550 5.1.1 User unknown
Monitoring Bounce Rate
from datetime import datetime, timedelta
def check_bounce_rate(days=7):
now = datetime.now()
start = now - timedelta(days=days)
params = {
"start_date": start.strftime("%Y-%m-%d"),
"end_date": now.strftime("%Y-%m-%d"),
"aggregated_by": "day"
}
response = sg.client.stats.get(query_params=params)
stats = response.to_dict()
total_sent = 0
total_bounces = 0
for day in stats:
metrics = day.get("stats", [{}])[0].get("metrics", {})
total_sent += metrics.get("requests", 0)
total_bounces += metrics.get("bounces", 0)
bounce_rate = (total_bounces / max(total_sent, 1)) * 100
print(f"Bounce rate (last {days} days): {bounce_rate:.2f}%")
print(f" Sent: {total_sent}")
print(f" Bounces: {total_bounces}")
if bounce_rate > 5:
print("CRITICAL: Bounce rate exceeds 5% — clean your list immediately!")
elif bounce_rate > 3:
print("WARNING: Bounce rate above 3% — review list hygiene")
else:
print("OK: Bounce rate is healthy")
check_bounce_rate()
# Expected output: Bounce rate (last 7 days): 0.8%
# OK: Bounce rate is healthy
Common Mistakes
1. Not Handling Bounce Webhooks
Ignoring bounce webhooks means you keep sending to invalid addresses, increasing your bounce rate and damaging reputation.
2. Confusing Soft and Hard Bounces
Soft bounces (mailbox full) should be retried. Hard bounces (address invalid) should be removed. Treating both the same wastes resources.
3. Re-sending to Bounced Addresses
Once an address hard bounces, sending again hurts your reputation. Remove it permanently unless the user re-verifies the address.
4. Not Monitoring Bounce Rate Trends
A sudden bounce rate spike indicates a problem (blacklisting, wrong list segment, ISP issue). Monitor daily bounce rates for anomalies.
5. Cleaning Bounce List Too Aggressively
SendGrid automatically suppresses bounced addresses. Deleting the bounce suppression allows sending to them again. Only clear if the address has been fixed.
Practice Questions
- What is the difference between hard bounce and soft bounce?
- How do you prevent sending to bounced addresses?
- What is a healthy bounce rate?
- How do you handle a block event?
Answers:
- Hard bounce: permanent failure (address doesn't exist). Soft bounce: temporary issue (mailbox full). Remove hard bounces, retry soft bounces.
- Process bounce webhooks and add addresses to a suppression list. Before sending, check the list. SendGrid also auto-suppresses bounces.
- Under 3% is good. Under 1% is excellent. Above 5% triggers ISP blocks and damages reputation.
- Investigate the block reason (spam content, blacklisted IP, rate limit). Check SendGrid's block list, review email content, and contact the receiving ISP if persistent.
Challenge: Build a bounce management system: process bounce webhooks to auto-remove hard bounces, retry soft bounces (up to 3 times), log all bounces with timestamps, monitor bounce rate daily, and alert if rate exceeds 3%.
FAQ
Mini Project
Build a bounce management and monitoring system: process bounce webhooks (classify hard/soft/block), auto-remove hard bounces from active lists, retry soft bounces with exponential backoff, generate weekly bounce rate reports, and set up alerts for bounce spikes.
What's Next
Event Webhook — receive real-time email delivery events via webhooks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro