$0.10 per 1,000 emails — by far the cheapest credible email API. The trade-off: more AWS-flavored setup (IAM, sandbox-mode exit request, DNS verification per region), and less polish than dev-first services. Right when per-email cost matters at scale.
The unit economics: SES sends 1,000 emails for $0.10. Resend sends 1,000 emails for $1.00 (10×). Postmark, $10 (100×). At 100K emails/month, the difference is $10 vs. $100 vs. $1,000. SES wins on cost; the others win on developer time.
Sign in to AWS Console. Search for SES. Pick the region closest to your users (e.g., us-east-1). SES console ↗.
SES is per-region — verifications and sending stats don't cross regions. Pick one and stick with it.
SES → Verified identities → Create identity → Domain. Type your domain. Pick Easy DKIM with RSA-2048.
SES generates 3 CNAME records. Add them at your DNS provider. SES auto-checks every 24h; usually verifies within an hour.
You can also verify a single email address (for testing) — Create identity → Email address → AWS sends a confirmation link. Quick but limited; production should use domain verification.
New SES accounts start in sandbox mode:
This is so AWS can vet you before you start sending to strangers. To exit, Account dashboard → Request production access. Fill in the form:
AWS responds within 24 hours, sometimes minutes. Approval brings limits to 50K/day initially, scaling automatically as you build reputation.
Don't use your AWS root credentials. Create an IAM user with a minimum permission policy:
ses-sender-prod (or similar).ses:SendEmail + ses:SendRawEmail).Save the access key + secret to env:
AWS_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_REGION=us-east-1
If your app already runs on AWS (EC2, Lambda, etc.), use an IAM Role instead of long-lived credentials. Standard AWS practice.
npm install @aws-sdk/client-sesv2
SES has v1 and v2 APIs. Use v2 — newer, better defaults, supports all current features. Other languages: boto3, AWS SDK for Java, Go, .NET ↗.
import {
SESv2Client,
SendEmailCommand,
} from "@aws-sdk/client-sesv2";
const ses = new SESv2Client({ region: process.env.AWS_REGION });
await ses.send(new SendEmailCommand({
FromEmailAddress: "[email protected]",
Destination: { ToAddresses: ["[email protected]"] },
Content: {
Simple: {
Subject: { Data: "Hello from AWS SES" },
Body: {
Text: { Data: "Plain text version" },
Html: { Data: "<p>HTML body</p>" },
},
},
},
}));
The AWS SDK auto-reads credentials from env vars or IAM Role. Check sending stats in the SES console → Reputation metrics.
AWS will suspend your SES account if your bounce rate exceeds 5% or complaint rate exceeds 0.1%. You must process these signals.
Standard setup:
default-prod.Pass the configuration set on each send:
await ses.send(new SendEmailCommand({
FromEmailAddress: "[email protected]",
Destination: { ToAddresses: ["..."] },
Content: { /* ... */ },
ConfigurationSetName: "default-prod",
}));
Without this, you'll get suspended at some point. Not optional.
SES supports server-side templates with Mustache-like syntax. Create:
import { CreateEmailTemplateCommand } from "@aws-sdk/client-sesv2";
await ses.send(new CreateEmailTemplateCommand({
TemplateName: "welcome",
TemplateContent: {
Subject: "Welcome to {{appName}}",
Html: "<p>Hi {{name}}, welcome!</p>",
Text: "Hi {{name}}, welcome!",
},
}));
Send with template:
import { SendEmailCommand } from "@aws-sdk/client-sesv2";
await ses.send(new SendEmailCommand({
FromEmailAddress: "[email protected]",
Destination: { ToAddresses: [user.email] },
Content: {
Template: {
TemplateName: "welcome",
TemplateData: JSON.stringify({ name: user.name, appName: "MyApp" }),
},
},
}));
SES templates are functional but the editor experience is minimal. Many teams render HTML in their app (with React Email, MJML, or similar) and pass the rendered string to Simple.Body.Html.
SES also offers SMTP credentials — useful when integrating with software that only speaks SMTP (WordPress, Postfix relays, legacy systems).
SES console → SMTP settings → Create SMTP credentials. Generates a username + password specific to SMTP. Connect to:
Host: email-smtp.us-east-1.amazonaws.com
Port: 587 (STARTTLS) or 465 (SSL)
Auth: the username/password just generated
SES console → Reputation metrics:
Set CloudWatch alarms on these. AWS will notify you well before suspension if you set them up right. Without alarms, you discover the problem when your sending stops working.
ses:SendEmail. Update the policy.$0.10 per 1,000 outbound emails. $0.10 per 1,000 inbound (if you receive). $0.12 per GB of attachments.
Free tier: 62,000 emails/month free if sent from an EC2 instance. Otherwise, paid from email 1 (but $0.10/1K is already so cheap it barely matters).