Email Delivery

Transactional Email Best Practices: Receipts, Alerts, and Password Resets

How to ensure transactional emails achieve near-100% delivery — separate sending infrastructure, dedicated IP pools, template design, and SLA monitoring.

Transactional vs. Marketing Email

Not all email is equal. Marketing email is scheduled, audience-targeted, and optional — users can unsubscribe without impact. Transactional email is triggered by user action, individually addressed, and often urgent:

TypeExamplesDelivery ToleranceUser Expectation
TransactionalPassword resets, order confirmations, 2FA codes, alertsNear-zeroArrives in < 30 seconds
MarketingNewsletters, promotions, drip campaignsMinutes to hoursExpected, optional

Mixing transactional and marketing email in the same sending stream is one of the most common and damaging mistakes. A spam complaint spike from a marketing campaign can land your entire sending domain in spam folders — including your password reset emails.

Infrastructure Separation

Dedicated Transactional Subdomain

Use a separate subdomain for transactional mail:

Transactional:  From: [email protected]
Marketing:      From: [email protected]

This subdomain isolation means the reputation of your marketing sending domain cannot affect your transactional domain. Configure separate DKIM keys and SPF records for each subdomain.

Separate IP Pool

Most ESPs support IP pool segmentation. Configure your ESP to route transactional mail through a dedicated IP pool:

# Amazon SES example: Configuration Set per email type
import boto3

ses = boto3.client("ses", region_name="us-east-1")

# Transactional email — dedicated IP pool
ses.send_email(
    Source="[email protected]",
    Destination={"ToAddresses": [recipient]},
    Message={...},
    ConfigurationSetName="transactional-config-set",  # Dedicated IP pool assigned here
)

# Marketing email — shared or separate marketing pool
ses.send_email(
    Source="[email protected]",
    Message={...},
    ConfigurationSetName="marketing-config-set",
)

Separate DKIM Keys

# Generate separate DKIM keys for each subdomain:
# Transactional DKIM:
mail._domainkey.mail.example.com. TXT "v=DKIM1; k=rsa; p=..."

# Marketing DKIM:
mail._domainkey.marketing.example.com. TXT "v=DKIM1; k=rsa; p=..."

Template Design for Transactional Email

Plain Text Fallback

Every transactional email should include a plain text alternative. Many email clients and security gateways prefer plain text for analysis, and it improves accessibility:

# Using Python's email library:
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

msg = MIMEMultipart("alternative")
msg["Subject"] = "Your password reset link"
msg["From"] = "[email protected]"
msg["To"] = recipient_email

plain = MIMEText(
    "Reset your password: https://example.com/reset?token=abc123\n\n"
    "This link expires in 15 minutes.",
    "plain"
)
html = MIMEText(html_content, "html")

# Attach plain text first — clients prefer the last matching part
msg.attach(plain)
msg.attach(html)

Minimal Images

Transactional email templates should be text-first. Heavy image use increases:

  • Spam score (image-only emails are a classic spam pattern)
  • Rendering failures (images may be blocked by default)
  • Message size (delays in mobile networks)

Limit to a single logo image. All critical information must be readable in plain text.

Avoiding Spam Trigger Words

Content filters scan transactional email too. Avoid:

# High-risk words in subject lines and body:
Free, Winner, Congratulations, Act Now, Limited Time, Urgent
"Click here" as link text (use descriptive text instead)
ALL CAPS in subject lines
Excessive punctuation (!!!, ???)

For password reset emails, a subject like Reset your example.com password is better than URGENT: Password Reset Required!!!

Unsubscribe Headers

Pure transactional emails (password resets, 2FA codes) do not require an unsubscribe link — they are system-triggered and cannot be opted out of. However, transactional emails with any marketing element (order confirmation + upsell) must include one:

List-Unsubscribe: <mailto:[email protected]?subject=unsub-order-emails>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

Priority Queuing and SLA Monitoring

Queue Priority

Password resets and 2FA codes have a de facto SLA of under 60 seconds. Configure your email queue to prioritize these:

# Django example with priority queuing:
from django_tasks import task

EMAIL_PRIORITY = {
    "password_reset": 1,    # Highest priority
    "2fa_code": 1,
    "order_confirmation": 2,
    "shipping_update": 3,
    "newsletter": 10,       # Lowest priority
}

@task(queue_name="email-critical")
def send_password_reset(user_id: int, token: str) -> None:
    ...

@task(queue_name="email-standard")
def send_order_confirmation(order_id: int) -> None:
    ...

Delivery Time SLA

Define and monitor delivery time targets:

Email TypeTarget DeliveryAlert Threshold
Password reset< 60 seconds> 2 minutes
2FA / OTP< 30 seconds> 90 seconds
Order confirmation< 2 minutes> 5 minutes
Shipping update< 5 minutes> 15 minutes

Alerting on Delivery Delays

# Track time-to-delivery via ESP webhooks:
from datetime import datetime, timezone

def handle_ses_delivery_event(event: dict) -> None:
    if event["eventType"] == "Delivery":
        sent_at = datetime.fromisoformat(event["mail"]["timestamp"])
        delivered_at = datetime.now(timezone.utc)
        delivery_time_seconds = (delivered_at - sent_at).total_seconds()

        email_type = event["mail"]["tags"].get("email_type", "unknown")
        threshold = SLA_THRESHOLDS.get(email_type, 300)

        if delivery_time_seconds > threshold:
            alert_on_call(f"{email_type} delivery took {delivery_time_seconds:.0f}s")

Provider Selection

Choose your transactional ESP based on your volume and operational requirements:

Cloud ESPs

ProviderStrengthsPricing
**Amazon SES**Cheapest at scale, AWS integration, high throughput$0.10/1K after 62K free/month
**Postmark**Best deliverability reputation, excellent dashboard, fast support$1.50/1K
**SendGrid**Feature-rich, good SDKs, Marketing + Transactional in one$0.85/1K
**Mailgun**Developer-friendly API, good SMTP gateway, EU data residency$0.80/1K

Recommendation: Amazon SES for cost-sensitive high-volume; Postmark for deliverability-critical applications where reputation management is paramount.

Self-Hosted (Postal)

Postal (postalserver.io) is an open-source mail server designed for transactional email. It provides SMTP + HTTP API sending, click/open tracking, bounce processing, and a web UI:

# Postal requires a dedicated server with a clean IP address
# Quick start with Docker:
git clone https://github.com/postalserver/postal.git
cd postal
docker-compose up -d

# Configure your sending domain, DKIM, and SPF in the UI
# then send via SMTP or HTTP API:
curl -X POST https://postal.yourdomain.com/api/v1/send/message \
  -H 'X-Server-API-Key: your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{"to": ["[email protected]"], "from": "[email protected]", "subject": "Test", "plain_body": "Hello"}'

Self-hosting gives full control but requires ongoing IP reputation management, blocklist monitoring, and mail server administration — significant operational overhead compared to managed ESPs.

Summary

The key to reliable transactional email delivery is separation: separate subdomain, separate IP pool, separate DKIM keys, and separate sending infrastructure from your marketing email. Back this with SLA monitoring and priority queuing for time-sensitive emails like password resets and 2FA codes. No marketing campaign should ever risk the delivery of a password reset that a user needs right now.

Related Protocols

More in Email Delivery