Prompt
How do I send receipt emails reliably at scale?
Latest observation
To send receipt emails reliably at scale, optimize for deliverability, queueing, idempotency, retries, and monitoring.
1) Separate sending from your main app flow
Don’t send emails inline in the request that creates the order/payment.
Use this flow:
- Payment/order is confirmed
- Write a
receipt_requestedevent to a database table or message queue - A background worker sends the email
- Mark the event as sent
This prevents slowdowns and lets you retry safely.
2) Use an email delivery provider built for scale
Use a reputable ESP such as:
- SendGrid
- Mailgun
- Postmark
- Amazon SES
- SparkPost
For receipt emails, transactional email is the right product category. It typically has better deliverability tooling and less risk of getting mixed with marketing mail.
3) Make sending idempotent
At scale, duplicate events happen.
Add a unique key like:
receipt:{order_id}- or
receipt:{payment_intent_id}
Before sending:
- check if this receipt was already sent
- or use a DB unique constraint / dedupe key
This avoids duplicate receipts when retries or message redelivery occur.
4) Queue and retry properly
Use a queue with:
- exponential backoff
- dead-letter queue
- bounded retries
- visibility timeouts if using a broker like SQS
Classify failures:
- Transient: provider timeout, 5xx, network errors → retry
- Permanent: invalid email, suppressed address, malformed template → do not retry forever
5) Authenticate your sending domain
Set up:
- SPF
- DKIM
- DMARC
Also:
- send from a domain you control, e.g.
receipts@yourdomain.com - align the “From” domain and DKIM signing domain
- use a real reply-to if needed
This is critical for inbox placement.
6) Keep the content simple and trustworthy
Receipt emails should be:
- plain, concise, and easy to parse
- include order number, items, total, taxes, date, and support info
- avoid spammy language and excessive images
- include both HTML and plain-text versions
Best practice:
- use a stable template
- don’t generate heavy content at send time
- include a unique receipt/order ID for support
7) Handle rate limits and provider failures
Even good providers throttle.
Implement:
- provider-side API rate limit handling
- retry-after support if present
- circuit breakers to avoid overwhelming a degraded provider
- fallback provider only if you can safely dedupe across providers
If you use multiple providers, make sure you don’t accidentally send duplicates.
8) Monitor delivery and bounces
Track:
- sent
- accepted by provider
- delivered
- bounced
- deferred
- complained
- opened/clicked only if you truly need it
Use provider webhooks to update your system:
- hard bounce → suppress future sends to that address
- complaint → suppress and alert
- deferral → retry later
Set alerts for:
- send failure rate spikes
- bounce rate spikes
- webhook lag
- queue depth growth
- high latency in send pipeline
9) Be careful with concurrency
If many workers process the same order stream:
- use row locking or a unique “sent” record
- use exactly-once-ish behavior via dedupe keys
- ensure only one worker can claim a receipt job
Common pattern:
status = pending- worker atomically claims job
- worker sends email
- worker marks
sent_at
10) Design for audits and customer support
Store:
- recipient
- template version
- provider message ID
- timestamp
- order/payment reference
- final status
This helps when a customer says “I never got the receipt.”
A solid reference architecture
App
- creates order/payment
- inserts
receipt_jobrow with unique constraint
Queue/Worker
- claims job
- renders template
- sends via ESP
- records provider ID and status
- retries transient failures
Provider webhooks
- update bounce/complaint/delivery state
- trigger alerts
Monitoring
- dashboards for send success, bounce rate, retry count, queue lag
Minimal implementation checklist
- Transactional email provider
- Dedicated receipt sender service
- Queue + worker
- Idempotency key per order/payment
- SPF/DKIM/DMARC
- Bounce/complaint webhooks
- Retry with backoff
- Metrics and alerts
- Suppression list handling
If you want, I can also give you:
- a reference architecture diagram,
- a database schema for idempotent receipt sending, or
- a sample implementation in Node/Python/Go.