Handling Transient Failures (Retry with Backoff)
August 2, 2026 · View on GitHub
The examples in this repository are intentionally minimal — they send one email and report success or failure. For a production sender (contact forms, password resets, notifications) that runs unattended, it's worth retrying on transient failures instead of dropping the message:
- SMTP
4xxresponse codes (e.g.421 Service not available,450 Mailbox busy) mean "try again later" — this is also how a rate limit rejection usually surfaces. - SMTP
5xxresponse codes (e.g.550 No such user) are permanent — retrying an unchanged message will fail again. Don't retry these; surface the error instead. - A dropped connection or timeout is worth one or two retries, since it's often a transient network blip.
Pattern: exponential backoff with jitter
Retry a handful of times with an increasing delay, plus a small random
jitter so multiple failed sends don't all retry at the exact same instant.
Reference implementation in Python, built on
python-zerosmtp.py:
import random
import time
from smtplib import SMTPResponseException, SMTPServerDisconnected
def send_with_retry(send_fn, max_attempts=4, base_delay=2.0):
"""Call send_fn() and retry on transient (4xx) SMTP failures.
Permanent (5xx) errors and non-SMTP exceptions are raised immediately.
"""
for attempt in range(1, max_attempts + 1):
try:
return send_fn()
except (SMTPResponseException, SMTPServerDisconnected) as e:
is_permanent = isinstance(e, SMTPResponseException) and e.smtp_code >= 500
if is_permanent or attempt == max_attempts:
raise
delay = base_delay * (2 ** (attempt - 1)) + random.uniform(0, 1)
time.sleep(delay)
# Usage:
# send_with_retry(lambda: send_email_via_zerosmtp(**config))
The same shape applies in any language: catch the transient-vs-permanent distinction from your SMTP library's exception/response code, retry only the transient case a bounded number of times, and always cap the total number of attempts — an unattended sender that retries forever on a permanently-misconfigured address is itself a bug.
What not to do
- Don't retry in a tight loop with no delay — that just recreates the load that triggered a rate limit in the first place.
- Don't silently swallow a permanent (5xx) failure — surface it so a bad recipient address gets fixed instead of retried forever.