AWS SES + Serverless: Email Delivery at Scale

- Published on
Every SaaS application eventually needs to send email: welcome messages, password resets, notifications. The default choice is often a third-party service like SendGrid or Mailgun. But when you are already on AWS, SES offers the same capabilities at a fraction of the cost. Here is how I set it up.
Hi there! Want to support me?
Domain Verification: The Critical First Step
Before SES can send email from your domain, you need to verify ownership. This involves adding DKIM records to your DNS. DKIM (DomainKeys Identified Mail) signs your emails cryptographically, proving they actually came from your domain and were not tampered with in transit.
// SST infrastructure for SES verified identityconst emailIdentity = new aws.sesv2.EmailIdentity('EmailIdentity', {emailIdentity: 'interviewsparring.com',})// Add DKIM verification records to Route53new aws.route53.Record('DkimRecord', {zoneId: hostedZone.id,name: emailIdentity.dkimSigningAttributes.apply(attrs => `${attrs.tokens?.[0]}._domainkey`),type: 'CNAME',ttl: 300,records: [emailIdentity.dkimSigningAttributes.apply(attrs => `${attrs.tokens?.[0]}.dkim.amazonses.com`)],})
With SST and infrastructure-as-code, the DKIM records are created automatically as part of deployment. No manual DNS configuration, no copy-pasting tokens from the AWS console.
Sending from Lambda
The @aws-sdk/client-sesv2 package makes sending email from a Lambda function straightforward. Here is a minimal example that sends a welcome email:
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'const ses = new SESv2Client({ region: 'us-east-1' })async function sendWelcomeEmail(to: string, name: string) {const command = new SendEmailCommand({FromEmailAddress: 'noreply@interviewsparring.com',Destination: { ToAddresses: [to] },Content: {Simple: {Subject: { Data: 'Welcome to Interview Sparring' },Body: {Html: { Data: `<h1>Welcome, ${name}!</h1><p>Start practicing today.</p>` },},},},ConfigurationSetName: 'interview-sparring-tracking',})return ses.send(command)}
A few things to note: the Lambda function needs IAM permissions for ses:SendEmail. In SST, this is handled by linking the Lambda to the SES identity resource. Also, I use a Configuration Set to track opens, clicks, bounces, and complaints.
Hi there! Want to support me?
Deliverability: Staying Out of Spam
Sending email is easy. Landing in the inbox is hard. Here are the non-negotiable practices:
- SPF and DKIM: Both must be configured correctly. SES guides you through this, but double-check with a tool like MXToolbox.
- Custom MAIL FROM domain: Use a subdomain like
bounces.interviewsparring.comso bounces go to your own domain, notamazonses.com. - Warm up new domains: If you are sending from a brand-new domain, start with low volume and ramp up over two weeks. SES has sandbox restrictions that force this anyway.
- Monitor your reputation: Use SES reputation dashboards and set up bounce/complaint notifications via SNS.
Cost Comparison
SES charges $0.10 per 1,000 emails. SendGrid starts at $19.95/month for 50,000 emails. At low to moderate volume, SES is essentially free. Even at 100,000 emails per month, SES costs $10 versus SendGrid's $19.95. The savings compound significantly at scale.
The trade-off is setup complexity. SES requires manual sandbox exit requests, domain verification, and more infrastructure work. If you are already on AWS and using infrastructure-as-code, this is a one-time cost that pays for itself quickly.
When NOT to Use SES
SES is not a marketing email platform. It lacks built-in templates, drag-and-drop editors, and campaign management. If you need those features, pair SES with a tool like Sendy, or use a dedicated platform like Mailchimp. For transactional email — password resets, notifications, welcome emails — SES is hard to beat on cost and reliability.