Sending bulk email with AWS SES without landing in spam
· 3 min read · Amrith Vengalath
- AWS
- SES
- Web
The brief was simple on paper: send order confirmations, password resets, and a monthly update to a few thousand customers. The hard part isn't sending email. Any script can send email. The hard part is the message actually landing in the inbox instead of the spam folder, and not getting your domain's reputation torched in the process.
We went with Amazon SES because the pricing was hard to argue with and we were already on AWS. Here's what I learned getting it working properly.
SES starts in a sandbox
The first surprise: a new SES account is sandboxed. You can only send to verified addresses, and your daily quota is tiny. That's fine for testing, but you have to request production access through a support request before you can send to real customers. Do that early - the approval isn't instant, and you don't want to discover the limit on launch day.
Sending itself is unremarkable:
const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses");
const ses = new SESClient({ region: "ap-south-1" });
await ses.send(new SendEmailCommand({
Source: "[email protected]",
Destination: { ToAddresses: [customer.email] },
Message: {
Subject: { Data: `Your order ${order.id} is confirmed` },
Body: { Html: { Data: renderOrderEmail(order) } },
},
}));The interesting work is everything around that call.
SPF, DKIM, DMARC - the three records that decide your fate
Mail providers ask three questions about every message: is the sender allowed to send for this domain, was the message tampered with, and what should I do if the answer is no. SPF, DKIM, and DMARC answer those.
- SPF is a DNS TXT record listing who's allowed to send mail for your domain. For SES you add their include. Skip it and Gmail gets suspicious immediately.
- DKIM signs each message with a private key; the public key lives in DNS so the receiver can verify nothing was altered. SES can manage this for you - turn on Easy DKIM and it gives you a few CNAME records to add.
- DMARC ties the two together and tells receivers what to do with mail that fails, plus where to send reports.
A reasonable starting DMARC record, kept gentle so you don't nuke legitimate mail while you're still learning:
v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100I started with p=none actually, just to collect reports and see what was failing, then moved to quarantine once I trusted the setup. Jumping straight to p=reject before you understand your own mail flow is a good way to silently lose real email.
Separate the streams
The mistake I almost made was sending order confirmations and marketing email from the same identity. Don't. A password reset that never arrives is a support ticket and a security problem; a newsletter that lands in spam is a Tuesday. They have different urgency and different sender reputations, so I split them - transactional from orders@, anything promotional from a separate subdomain. If the marketing reputation takes a hit, your receipts still go through.
Watch the bounces
SES will happily let your reputation degrade if you keep mailing addresses that bounce. Wire up the bounce and complaint notifications (SNS topic into a small handler) and actually remove those addresses from your list. I treated a hard bounce as "never send here again" and a complaint as "remove from everything immediately." It's tedious but it's the difference between a healthy sender score and getting throttled.
What I'd tell myself starting out
Get the DNS records right before you send a single real message - test with a tool that scores SPF/DKIM/DMARC. Warm up slowly rather than blasting your whole list on day one. And keep transactional and marketing mail apart. None of it is glamorous, but inbox placement is mostly a reputation game, and reputation is easier to keep than to rebuild.