DEV Community

Cover image for Amazon SES Setup: The Things Nobody Tells You (Bounce Handling, DKIM, IAM)
Shreyash
Shreyash

Posted on • Originally published at devdecide.com

Amazon SES Setup: The Things Nobody Tells You (Bounce Handling, DKIM, IAM)

📖 Full guide with all code snippets and DNS records: Amazon SES Setup — The Complete Guide


Every Amazon SES tutorial on Google shows outdated screenshots and zero mention of what happens when your bounce rate spikes and AWS silently kills your account.

This post covers the parts that actually bite you in production.


Why SES? The Math Is Simple

Provider 100,000 emails/month
SendGrid ~$90
Amazon SES ~$10

That's not a rounding error. One order of magnitude cheaper. For a bootstrapped SaaS sending transactional emails, that compounds fast.

The trade-off: AWS does not hold your hand. Misconfigure it and your emails land in spam — or your account gets permanently revoked.


Step 1: Understand the SES Sandbox

Every new AWS account starts sandboxed. Hard limits apply:

  • Max 200 emails per 24 hours
  • Max 1 email per second
  • You can only send to pre-verified email addresses

To escape the Sandbox, open a support case under Service Limit Increase → SES Sending Limits.

⚠️ Don't submit this ticket yet. Build your bounce handling infrastructure first (Step 3). AWS will ask how you handle bounces — you need to already have an answer.


Step 2: Verified Identity + DKIM

Go to SES Console → Verified Identities → Create Identity → Domain.

AWS generates 3 CNAME records for DKIM. Add them to your DNS:

Name:  token1._domainkey.yourdomain.com
Type:  CNAME
Value: token1.dkim.amazonses.com
Enter fullscreen mode Exit fullscreen mode

Also add SPF and DMARC records. All three working together is what gives your domain a clean sender reputation from day one. Without DMARC, even a correctly DKIM-signed email can get flagged.


Step 3: Configuration Sets — The Most Skipped Step

This is where most developers fail. Here are the limits AWS enforces:

Metric Probation Suspension
Bounce Rate 5% 10%
Complaint Rate 0.08% 0.1%

When you hit 10% bounces, SES suspends you instantly with no warning. Reactivating takes a manual support ticket and days of waiting.

Configuration Sets are the fix. They route bounce and complaint events to an SNS topic → your webhook → your database.

Setup in 5 steps:

  1. SES → Configuration Sets → Create → name it production-transactional
  2. Add destination → choose SNS
  3. Check Hard Bounces and Complaints
  4. Create SNS topic ses-bounces-topic
  5. Add an HTTPS subscription pointing to your webhook URL

Every email you send must include ConfigurationSetName in the API call. If you omit it, bounces are not tracked at all.


Step 4: Sending Email (Node.js SDK v3)

import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";

const sesClient = new SESClient({ region: "us-east-1" });

export async function sendTransactionalEmail(toAddress, subject, htmlBody) {
  const params = {
    Source: "hello@yourdomain.com",
    Destination: { ToAddresses: [toAddress] },
    Message: {
      Subject: { Data: subject, Charset: "UTF-8" },
      Body: { Html: { Data: htmlBody, Charset: "UTF-8" } },
    },
    ConfigurationSetName: "production-transactional", // Never omit this
  };

  const command = new SendEmailCommand(params);
  const response = await sesClient.send(command);
  return response.MessageId;
}
Enter fullscreen mode Exit fullscreen mode

Same parameters map directly to Boto3 (Python), PHP, Ruby, or any official AWS SDK.


Step 5: Handle the Bounce Webhook

When SNS fires your webhook, it sends a SubscriptionConfirmation POST first. You must auto-confirm it or real events never arrive.

export async function POST(request) {
  const snsMessage = JSON.parse(await request.text());

  // Confirm subscription on first contact
  if (snsMessage.Type === 'SubscriptionConfirmation') {
    await fetch(snsMessage.SubscribeURL);
    return Response.json({ status: 'confirmed' });
  }

  if (snsMessage.Type === 'Notification') {
    const sesEvent = JSON.parse(snsMessage.Message);
    if (sesEvent.notificationType === 'Bounce') {
      const affected = sesEvent.bounce.bouncedRecipients.map(r => r.emailAddress);
      // Blacklist these in your DB — never email them again
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then guard every send:

if (!user?.emailDeliverable) return; // Skip blacklisted addresses
Enter fullscreen mode Exit fullscreen mode

This single check keeps your bounce rate clean at scale.


The IAM Policy (Least Privilege)

Give your app credentials only what they need:

{
  "Statement": [{
    "Effect": "Allow",
    "Action": ["ses:SendEmail", "ses:SendRawEmail"],
    "Resource": "*"
  }]
}
Enter fullscreen mode Exit fullscreen mode

No identity management. No billing access. If credentials leak, blast radius is contained.


4 Mistakes to Avoid

  1. Omitting ConfigurationSetName — AWS won't throw an error, but bounce tracking is dead
  2. Verifying the wrong domainmail.yourdomain.comyourdomain.com in SES
  3. Not confirming the SNS subscription — bounce notifications never arrive
  4. Mismatched AWS regions — your SESClient region must match where your identity was created

When SES Is NOT the Right Choice

  • Under 10,000 emails/month? Use Resend or Postmark — the DX is smoother and cost difference is negligible
  • No backend experience on your team? The SNS + IAM setup will cost more in debugging hours than you save
  • Need open rates, click tracking, A/B tests? SES gives you raw events. You build the dashboards yourself.

Come to SES when the invoices start to hurt.


The Bottom Line

SES's reputation for being painful is earned — but the engineering discipline it demands is exactly what keeps your infrastructure cheap and stable at scale.

$10 per 100,000 sends vs $90 elsewhere. Do the setup right once, and you never think about it again.


Want the full walkthrough with every DNS record, the complete webhook code, and the exact console steps?

👉 Amazon SES Setup: The Complete Guide — DevDecide

Top comments (0)