Moving Production Email to Amazon SES: The Checklist That Actually Matters
Amazon SES is cheap because deliverability is your job. The production checklist we used: custom MAIL FROM, DMARC alignment, configuration sets, webhook security and tiered suppression.

On this page (8)
- What Amazon SES is, and what it is not
- DMARC at p=reject rides on a single signature until you fix MAIL FROM
- Separate marketing from transactional before your first send
- Wire bounce and complaint events before real traffic, not after
- Treat your webhook like an attacker knows the URL
- Test with the mailbox simulator, not your own inbox
- The numbers AWS actually enforces
- The checklist
Ask around about Amazon SES and you hear two things. It is very cheap, about $0.10 per thousand emails. And it does almost nothing for you. Both are true, and the second one is the whole story of moving to it.
We just finished moving SendHeron's own production sending to SES in eu-central-1, the Frankfurt region. Real lifecycle sequences, real transactional mail, real bounce handling. Nothing in this article is theory. It is the checklist we used, in the order that matters, with the parts that bite explained before they bite you.
What Amazon SES is, and what it is not
Amazon SES is the raw sending layer underneath a surprising number of email products. You verify a domain, you get an API and an SMTP endpoint, and Amazon delivers your mail from infrastructure with a good reputation. That is the product. There is no sequence builder, no contact list, no unsubscribe page, no analytics dashboard worth the name.
That trade is fine if you know you are making it. SES handles delivery. Everything that keeps delivery healthy stays your job: authentication, bounce processing, complaint processing, suppression, stream separation, testing. Miss one of those and SES will happily keep sending until AWS pauses your account.
The rest of this article is those jobs, one by one.
DMARC at p=reject rides on a single signature until you fix MAIL FROM
This is the one that almost nobody tells you about, so it goes first.
DMARC passes when at least one of two checks passes with alignment. Either DKIM: the mail carries a valid signature from your domain. Or SPF with alignment: the envelope sender domain, called the MAIL FROM or Return-Path, matches your domain.
Out of the box, SES sends with its own envelope. Your recipient sees your address, but the Return-Path is an amazonses.com address. SPF passes for Amazon's domain, not yours, so it does not align. Result: your DMARC verdict depends entirely on DKIM.
Most days that is invisible. Then a DNS provider hiccups on a CNAME, or someone rotates DKIM keys carelessly, and every email you send starts failing DMARC at once. If your policy is a dmarc policy of reject, receivers do exactly what you asked them to do. They reject everything. There is no partial credit and no warning period.
The fix is a custom MAIL FROM domain, and it takes about ten minutes:
- Pick a subdomain, something like
bounce.yourdomain.com. - In SES, set it as the MAIL FROM domain for your verified identity.
- Add the MX record SES gives you, pointing at
feedback-smtp.<region>.amazonses.com. - Add a TXT record on the subdomain:
v=spf1 include:amazonses.com ~all.
Now SPF aligns with your domain too, and DMARC has two legs to stand on instead of one. If you run p=reject, or ever plan to, do this before your first production send. You can check whether your own domain has this gap with our free email deliverability test, which flags a DMARC that currently passes on DKIM alone. We wrote up the full SPF, DKIM and DMARC setup separately if you want the deeper version.
Separate marketing from transactional before your first send
A password reset and a product newsletter should never share a fate. If a campaign goes out to a stale segment and picks up complaints, that is a problem. If it also drags down delivery of receipts and password resets, that is an outage.
SES gives you the tool for this: configuration sets. A configuration set is a named bundle of settings and event wiring that you attach to each send. Create two, one for marketing and one for transactional, and pass the right one on every single API call. Add a message tag with the stream name as well, so every event record that comes back tells you which stream it belongs to.
This costs you nothing on day one and buys you two things forever. Your reputation metrics stay separated per stream, so you can see a marketing problem clearly instead of a blended average hiding it. And your event pipeline can treat the streams differently, which matters for suppression policy below.
We went one step further and made the separation a property of the platform rather than a convention, so a campaign physically cannot go out on the transactional stream. If you are building on raw SES, the discipline of always passing the configuration set is the minimum version of the same idea. There is more on how we split the streams on the transactional and marketing page.
Wire bounce and complaint events before real traffic, not after
SES reports delivery, bounce and complaint events through SNS, Amazon's notification service. You create a topic per stream, point the configuration set's event destination at it, and subscribe your webhook endpoint to the topic.
The order matters. Events start flowing the moment the wiring exists, and only from that moment. Every hard bounce that happens before your webhook is live is a bounce you never heard about, an address you will happily mail again, and a small permanent dent in your reputation. Wire events first, send second.
Two details we got right and would repeat:
Persist the delivery events, not just the failures. A delivered timestamp on every message is the difference between a real delivery rate and a guess. It also means that when someone says they never got an email, you can answer with facts instead of resending blind.
Suppress in tiers. A hard bounce or a spam complaint should block everything, including transactional mail, because the address is either dead or hostile. An unsubscribe should block marketing only. Someone who opted out of your newsletter still wants their password reset. Collapsing those tiers into one list is how password resets end up silently dropped.
Treat your webhook like an attacker knows the URL
Your SNS webhook endpoint is a public URL that mutates email state. Think about what that means. If it trusts whatever gets POSTed to it, anyone who finds the URL can forge a complaint event and put any address they like on your suppression list. Forge enough of them and your own customers stop receiving mail, and nothing in your logs looks unusual.
Two checks close this:
- Verify the SNS message signature. Every SNS message is signed. Fetch Amazon's signing certificate, check the certificate URL actually points at an AWS SNS host before you fetch it, and validate the signature before you trust a byte of the payload.
- Check the topic. A valid signature only proves the message came from SNS. It does not prove it came from your topic. Someone can subscribe your endpoint to their own SNS topic and send you perfectly signed garbage. Keep an allowlist of your own topic ARNs and drop everything else.
Neither check is hard. Both are the kind of thing that feels paranoid right up until the day it is not.
Test with the mailbox simulator, not your own inbox
Sending test mail to your own Gmail proves almost nothing, and testing bounces by mailing made-up addresses actively hurts you, because those bounces count against your reputation.
SES ships a mailbox simulator for exactly this. Send to [email protected] for a clean delivery, [email protected] for a hard bounce, and [email protected] for a spam complaint. The events flow through your real pipeline, SNS, webhook, suppression and all, and none of it touches your reputation metrics.
Before we flipped production traffic, the test that mattered was one simulator bounce arriving as a signed SNS event, passing the topic check, and landing in the suppression list on the right tier. That single path proves the whole loop.
The numbers AWS actually enforces
Amazon SES deliverability is not just about inbox placement. AWS itself watches two numbers per account, and documents the thresholds:
| Metric | Review starts | Sending at risk |
|---|---|---|
| Bounce rate | 5% | 10% |
| Complaint rate | 0.1% | 0.5% |
Gmail and Yahoo run their own bulk sender rules on top for anyone sending 5,000 or more emails a day to them: SPF and DKIM both in place, an aligned DMARC policy, one-click unsubscribe, and a spam rate kept under 0.3%.
The practical reading of all these numbers is the same. Bounces come from list hygiene, so suppress hard bounces instantly and never buy addresses. Complaints come from consent and relevance, so make unsubscribing effortless and send less than you think you should. The thresholds are generous to anyone doing the basics honestly and brutal to anyone who is not.
The checklist
In order, before the first production send:
- Verify your domain identity, DKIM enabled.
- Set a custom MAIL FROM subdomain, with its MX and SPF records, so DMARC has both legs.
- Confirm your DMARC record and know what your policy does when checks fail.
- Create two configuration sets, marketing and transactional, and pass one on every send.
- Create the SNS topics and event destinations for bounce, complaint and delivery events, per stream.
- Stand up the webhook with signature verification and a topic allowlist.
- Build tiered suppression: hard blocks everything, consent blocks marketing only.
- Run the mailbox simulator through the whole path and watch a bounce land in suppression.
- Flip real traffic. Keep the old provider's credentials around for a week as a rollback.
- Watch bounce and complaint rates daily for the first weeks, per stream, not blended.
Steps 1 to 3 are verifiable from the outside with the SPF, DKIM and DMARC checkers. None of this is exotic. It is a few days of careful work that most teams do in the wrong order, after the first incident instead of before it.
SendHeron is lifecycle email automation that runs on exactly the setup described above, EU-hosted, with the stream separation and tiered suppression built in. We are in private beta and onboarding a small number of teams at a time. Join the waitlist if you would rather not build all ten steps yourself.