Tutorials / Backend integrations / Send transactional email with AWS SES
📝 Written ● Intermediate Updated 2026-05-13

Send transactional email with AWS SES

$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.

When to pick SES

0
  • Pick SES when — you send a lot of email (100K+/month), you're already on AWS, or cost is the #1 factor.
  • Pick Resend for fastest setup + dev-first DX at small/medium scale.
  • Pick Postmark if best inbox placement matters most.
  • Pick SendGrid for big enterprise feature set.

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.

Activate SES in your AWS account

1

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.

Verify your sending domain

2

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.

Get out of sandbox mode

3

New SES accounts start in sandbox mode:

  • You can only send to verified email addresses.
  • 200 emails/day limit.
  • 1 email/second rate.

This is so AWS can vet you before you start sending to strangers. To exit, Account dashboard → Request production access. Fill in the form:

  • Mail type — Transactional.
  • Website URL — your product.
  • Use case description — be specific. "Password reset emails and receipts for users of yourapp.com." Vague answers get rejected.
  • Bounce / complaint handling — describe your plan (Step 7).

AWS responds within 24 hours, sometimes minutes. Approval brings limits to 50K/day initially, scaling automatically as you build reputation.

Create IAM credentials for SES

4

Don't use your AWS root credentials. Create an IAM user with a minimum permission policy:

  1. IAM console ↗Users → Create user.
  2. Name: ses-sender-prod (or similar).
  3. Attach policies directly → AmazonSESFullAccess (or scope further with a custom inline policy for only ses:SendEmail + ses:SendRawEmail).
  4. Create user → Security credentials tab → Create access key → Application running outside AWS.

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.

Install the SDK

5
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 ↗.

Send your first email

6
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.

Handle bounces + complaints (mandatory)

7

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:

  1. SES → Configuration sets → Create a configuration set named e.g. default-prod.
  2. Configure Event destinations → SNS topic for Bounce + Complaint events.
  3. In your app, subscribe an HTTP endpoint to the SNS topic. SNS HTTP subscribers ↗.
  4. When a bounce/complaint event arrives, mark the recipient as unsendable in your DB. Stop sending to them.

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.

Templates (if you want them)

8

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.

SMTP interface (if you can't use the SDK)

9

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

SMTP docs ↗.

Monitor reputation

10

SES console → Reputation metrics:

  • Bounce rate — keep under 5%. Suspension at 10%.
  • Complaint rate — keep under 0.1%. Suspension at 0.5%.
  • Spam complaints — anyone marking your mail as spam in their client.

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.

Common failures

11
  • "Email address not verified" — you're still in sandbox mode, or sending from an unverified address. Verify domain (Step 2) or exit sandbox (Step 3).
  • "AccessDenied" — IAM user lacks ses:SendEmail. Update the policy.
  • "MessageRejected" — body too large (10 MB limit), unsupported character encoding, or the configuration set doesn't exist.
  • "Throttling" — exceeded your account's send rate. Check current limits at Account dashboard. Auto-scaling kicks in over time as reputation builds.
  • Sandbox-mode emails arriving but production-mode emails missing — region mismatch. SES is per-region; client must use the same region as your verifications.
SES is the AWS-iest of email APIs. Expect to learn some IAM, CloudWatch, and SNS along the way. If you're not already comfortable with AWS, Resend or Postmark will get you sending in 5 minutes; SES might take 2 hours. The payoff is 10–100× cheaper per email at scale.

Pricing reality

12

$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).

SES pricing ↗.

Official references

What's next