Blog
How to handle Amazon SES bounces and complaints with SNS
October 6, 2026 · 9 min read
Amazon SES will happily send to an address that bounced yesterday, and keep doing it until your bounce rate gets the account reviewed. Handling bounces and complaints means getting SES to tell you what happened and acting on it. Here is the whole setup and what each piece is for.
Why this matters
Mailbox providers and AWS both watch two numbers: the share of your mail that bounces and the share that recipients mark as spam. AWS puts an account under review when the bounce rate reaches about 5% or the complaint rate about 0.1%, and can pause sending if it keeps climbing. Check AWS's current thresholds, but the lesson is the same: you need to know about every bounce and complaint and stop mailing those addresses.
How the pieces fit
- A configuration set with an event destination: tells SES to publish events for mail sent with that set.
- An SNS topic that SES publishes those events to.
- An HTTPS subscription: SNS posts each event to an endpoint of yours.
- Your endpoint, which verifies the message, reads the event and updates a suppression list.
- A check before every send against that list.
1. Create the topic and let SES publish to it
Create an SNS topic in the same region as your SES identity. Then attach a policy that lets SES publish to it, or the event destination in the next step will silently receive nothing:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ses.amazonaws.com" },
"Action": "sns:Publish",
"Resource": "arn:aws:sns:us-east-1:111122223333:ses-events",
"Condition": { "StringEquals": { "AWS:SourceAccount": "111122223333" } }
}]
}2. Create the configuration set
import { SESv2Client, CreateConfigurationSetCommand,
CreateConfigurationSetEventDestinationCommand } from "@aws-sdk/client-sesv2";
const ses = new SESv2Client({ region: "us-east-1" });
await ses.send(new CreateConfigurationSetCommand({ ConfigurationSetName: "app-events" }));
await ses.send(new CreateConfigurationSetEventDestinationCommand({
ConfigurationSetName: "app-events",
EventDestinationName: "to-sns",
EventDestination: {
Enabled: true,
MatchingEventTypes: ["BOUNCE", "COMPLAINT", "DELIVERY"],
SnsDestination: { TopicArn: "arn:aws:sns:us-east-1:111122223333:ses-events" },
},
}));Send BOUNCE, COMPLAINT and DELIVERY at a minimum. Delivery events let you confirm that mail arrived, not only that it failed.
3. Send with the configuration set
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
await new SESv2Client({ region: "us-east-1" }).send(new SendEmailCommand({
FromEmailAddress: "hello@example.com",
Destination: { ToAddresses: ["user@example.org"] },
Content: { Simple: { Subject: { Data: "Hi" }, Body: { Text: { Data: "Hello" } } } },
ConfigurationSetName: "app-events", // without this, no events are published
}));The most common mistake
A configuration set only applies to messages that name it. Forget ConfigurationSetName on one code path and its bounces vanish. You can set a default configuration set on the identity to guard against this.
4. Subscribe your endpoint
Subscribe an HTTPS URL to the topic. SNS first sends a SubscriptionConfirmation message to that URL, and the subscription does nothing until you confirm it, either by calling ConfirmSubscription with the token or by fetching the SubscribeURL it contains. Only confirm messages that pass the check in the next step.
5. Verify every message
Your endpoint is a public URL, so anyone can post to it. SNS signs each message, and you must check the signature before trusting anything in it:
- Require that
SigningCertURLis HTTPS and its host is an official SNS host such assns.us-east-1.amazonaws.com. Never fetch an arbitrary URL from the message. - Rebuild the string to sign from the fields AWS specifies for that message type, in the specified order, and check
Signatureagainst the certificate using the algorithm named bySignatureVersion. - Check that the topic ARN is the one you expect.
This is the part most home-built webhooks skip. Without it, anyone who finds the URL can suppress your customers or fake bounces.
6. Read the event
SNS wraps the SES event as a JSON string in the Message field. After you parse it you get something like:
{
"eventType": "Bounce",
"mail": { "messageId": "0100018c-...", "destination": ["user@example.org"] },
"bounce": {
"bounceType": "Permanent",
"bounceSubType": "General",
"bouncedRecipients": [{ "emailAddress": "user@example.org" }]
}
}- Permanent bounce: the address doesn't exist or can't receive mail. Suppress it.
- Transient bounce (full mailbox, temporary failure): don't suppress on the first one. Retrying later can work.
- Complaint: the recipient marked it as spam. Suppress it immediately, and never email them again.
Match each event to your own record of the email with mail.messageId, so you can show a user what happened to a message.
7. Suppress before you send
Store the suppressed addresses (lowercase them) and look each one up before every send, in every code path that sends. SES also keeps an account-level suppression list that it can fill automatically for bounces and complaints, which is a useful safety net. A list of your own gives you the history and lets you handle it per customer.
What else you'll want
- Make the endpoint idempotent: SNS can deliver the same message more than once.
- Return a 2xx quickly and do the heavy work afterwards, or SNS will retry.
- Alert when your bounce or complaint rate rises, before AWS notices.
- Get production access first if you haven't: the sandbox guide covers it.