Amazon SES SNS Feedback Loop: Complete Setup Guide

Isometric illustration of a teal SES pillar emitting event arrows into a slate gray SNS queue with a purple notification bell accent

Amazon SES sends roughly 50+ pieces of metadata about every message it touches - delivery timestamps, bounce subcodes, complaint feedback types, remote MTA IPs - but none of it reaches your application until you wire up an SNS topic. Without that feedback loop, hard bounces accumulate on the SES account-level suppression list while your own database keeps trying to mail dead addresses, your reputation quietly degrades, and the first sign of trouble is usually an account suspension email.

This guide walks through every part of the SNS feedback loop for SES: which events SES can emit, the two configuration paths (identity vs. configuration set), the exact topic access policy, the JSON payload your subscriber will parse, and the production patterns we see fail most often. It's written for developers running raw SES or building custom integrations. If you use Mailblast, the BYO-SES management layer this blog is published on, the feedback loop is wired up for you - this is the plumbing underneath.

What SNS feedback covers and what it doesn't

SES can publish three notification types to SNS via identity notifications - Bounce, Complaint, and Delivery - and a wider set of event types via configuration set event destinations: send, reject, bounce, complaint, delivery, open, click, renderingFailure, deliveryDelay, and subscription. The two mechanisms are independent and can be used together. Identity notifications fire for every message sent from a verified identity; event destinations fire only for messages tagged with a configuration set.

There are two things SNS feedback does not give you. First, it isn't how SES enforces suppression - the account-level suppression list silently drops sends to known-bad addresses whether or not you subscribe to notifications. Second, soft bounces don't trigger a bounce notification unless SES gives up retrying; if you want every transient failure as it happens, you need deliveryDelay events from a configuration set.

Event coverage: identity notifications vs configuration set event destinations SES event coverage by configuration path Which events each mechanism can publish to SNS Identity notifications Configuration set event destinations Bounce Complaint Delivery Send Reject Open / Click DeliveryDelay RenderingFailure - - - - -
Source: AWS, Configuring Amazon SNS notifications for Amazon SES (2026).

Two paths: identity notifications vs configuration set event destinations

The two configuration paths solve different problems. Identity notifications are scoped to a verified domain or email and capture the three core feedback events for every message sent from that identity - you can't opt out per-message. Configuration set event destinations are scoped per-message: you tag a SendEmail call with a configuration set, and SES emits the events that set has enabled to its configured destinations (SNS, Kinesis Firehose, CloudWatch, EventBridge, or Pinpoint).

Most teams need both. Identity notifications guarantee you'll never miss a bounce on a sandbox or admin email that forgot its config set. Event destinations give you per-campaign separation, engagement tracking, and the wider event types like deliveryDelay and renderingFailure.

Aspect Identity notifications Configuration set event destinations
Scope Per verified domain or email Per message (set via X-SES-CONFIGURATION-SET header)
Event types Bounce, Complaint, Delivery Send, Reject, Bounce, Complaint, Delivery, Open, Click, RenderingFailure, DeliveryDelay, Subscription
Top-level JSON key notificationType eventType
Destinations SNS only SNS, Kinesis Data Firehose, CloudWatch, EventBridge, Pinpoint
API call SetIdentityNotificationTopic CreateConfigurationSetEventDestination
Opt-out per message No Yes - by omitting the header
Best for Account-wide safety net for bounces and complaints Per-campaign analytics, engagement, granular failure events
Source: AWS SES Developer Guide, Configuring Amazon SNS notifications (2026).

Setting up an SNS topic for SES feedback

Create the SNS topic before you touch SES, because SES validates the topic ARN and access policy at configuration time. The topic must be Standard type (SES does not support FIFO), it must be in the same AWS Region as the SES identity, and its access policy must explicitly allow ses.amazonaws.com to publish.

Create the topic with the AWS CLI:

aws sns create-topic \
  --name ses-feedback-prod \
  --region us-east-1 \
  --attributes DisplayName="SES Feedback"

Then attach an access policy that scopes publishing to your account and the specific SES identity (the AWS:SourceArn condition is the important guardrail - without it, any SES account in the world that knows your topic ARN could publish to it):

{
  "Version": "2012-10-17",
  "Id": "ses-feedback-policy",
  "Statement": [
    {
      "Sid": "AllowSESPublish",
      "Effect": "Allow",
      "Principal": { "Service": "ses.amazonaws.com" },
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:us-east-1:111122223333:ses-feedback-prod",
      "Condition": {
        "StringEquals": {
          "AWS:SourceAccount": "111122223333",
          "AWS:SourceArn": "arn:aws:ses:us-east-1:111122223333:identity/example.com"
        }
      }
    }
  ]
}

If your topic uses AWS KMS server-side encryption, you also need to add kms:GenerateDataKey and kms:Decrypt permissions for ses.amazonaws.com to the key policy, or SES will fail with InvalidParameterValue when you try to attach the topic.

Finally, wire SES to the topic. For identity notifications, the cleanest path is the SetIdentityNotificationTopic API:

aws ses set-identity-notification-topic \
  --identity example.com \
  --notification-type Bounce \
  --sns-topic arn:aws:sns:us-east-1:111122223333:ses-feedback-prod

aws ses set-identity-notification-topic \
  --identity example.com \
  --notification-type Complaint \
  --sns-topic arn:aws:sns:us-east-1:111122223333:ses-feedback-prod

At minimum, subscribe Bounce and Complaint. Delivery notifications are useful for debugging but expensive at volume since they fire for every accepted message. Once SNS is wired up, disable email feedback forwarding for the identity (SetIdentityFeedbackForwardingEnabled) so you don't get double-notified.

Subscribing your application (HTTPS endpoint vs SQS vs Lambda)

SNS supports multiple subscriber types and the right choice depends on how you want to handle backpressure and retries. HTTPS endpoints are the simplest to wire up but require your endpoint to be publicly reachable and to handle SNS subscription confirmation. SQS gives you a durable buffer and lets you process at your own pace. Lambda is fully managed and integrates cleanly with downstream services like DynamoDB.

For most production systems, SQS is the right default. It survives subscriber outages, gives you a natural dead-letter queue, and decouples notification arrival from processing. The pattern:

# Create the processing queue and a DLQ
aws sqs create-queue --queue-name ses-feedback
aws sqs create-queue --queue-name ses-feedback-dlq

# Wire the DLQ as a redrive policy on the main queue (5 retries)
# Then subscribe the queue to the SNS topic
aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:111122223333:ses-feedback-prod \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:us-east-1:111122223333:ses-feedback

If you go the Lambda route, here's a minimal Node.js handler that handles both notification shapes (raw delivery off and on):

exports.handler = async (event) => {
  for (const record of event.Records) {
    // SNS -> Lambda: payload is in record.Sns.Message as a JSON string
    const payload = JSON.parse(record.Sns.Message);
    const type = payload.notificationType || payload.eventType;

    switch (type) {
      case "Bounce":
        await handleBounce(payload.bounce, payload.mail);
        break;
      case "Complaint":
        await handleComplaint(payload.complaint, payload.mail);
        break;
      case "Delivery":
        await handleDelivery(payload.delivery, payload.mail);
        break;
      default:
        console.warn("Unhandled type", type);
    }
  }
  return { ok: true };
};

If you use raw HTTPS, your endpoint must respond 200 OK within 15 seconds and handle a SubscriptionConfirmation POST on first subscribe by fetching the SubscribeURL field. Skipping that step is the #1 reason "my endpoint isn't getting notifications" - the subscription is still in PendingConfirmation.

Parsing the bounce notification payload

The top-level JSON object always contains notificationType (or eventType for configuration set events), a mail object describing the original message, and one of bounce, complaint, or delivery depending on the type. SES explicitly reserves the right to add fields, so your parser must ignore unknown keys.

Here's a real hard bounce payload, abbreviated:

{
  "notificationType": "Bounce",
  "mail": {
    "timestamp": "2026-07-22T14:05:45.000Z",
    "messageId": "0102018f7a-...-000000",
    "source": "campaigns@example.com",
    "sourceArn": "arn:aws:ses:us-east-1:111122223333:identity/example.com",
    "sendingAccountId": "111122223333",
    "destination": ["recipient@example.com"]
  },
  "bounce": {
    "bounceType": "Permanent",
    "bounceSubType": "General",
    "bouncedRecipients": [
      {
        "emailAddress": "recipient@example.com",
        "action": "failed",
        "status": "5.1.1",
        "diagnosticCode": "smtp; 550 5.1.1 user unknown"
      }
    ],
    "timestamp": "2026-07-22T14:05:46.605Z",
    "feedbackId": "0102018f7a-...-000000",
    "reportingMTA": "dsn; a8-70.smtp-out.amazonses.com",
    "remoteMtaIp": "203.0.113.42"
  }
}

The fields that drive real decisions:

  • bounce.bounceType - Permanent means remove the address now; Transient means SES already retried and gave up; Undetermined means the remote MTA didn't give enough information.
  • bounce.bounceSubType - the actionable detail. General is a normal hard bounce. Suppressed means the address is on the SES global suppression list. OnAccountSuppressionList means your own account list dropped it (this does not count toward your bounce rate). MailboxFull is transient-recoverable.
  • bouncedRecipients[].diagnosticCode - the raw SMTP response from the remote MTA. Log this verbatim for support cases.

For complaints, the payload is simpler - complainedRecipients[].emailAddress is the only field that drives an action. Most ISPs strip the recipient address from the feedback report for privacy, so SES does a best-effort lookup against the original destinations. The mailbox provider name is not in the payload; you need to infer it from the recipient domain. See our deeper guide on bounce and complaint handling for the full rules engine.

Common production patterns

After processing thousands of SES feedback events for Mailblast customers, three patterns show up everywhere worth getting right.

Suppression list synchronization. Hard bounces and complaints must update your own subscriber database. SES has already added the address to its account-level suppression list and will silently drop future sends, but your application will keep trying - and every send that gets silently dropped wastes a SES SendEmail call and shows up in your CloudWatch send count without a matching delivery. Mark the contact bounced_at or complained_at in your DB inside the SNS handler, and gate every future send on that column.

Alerting on bounce rate spikes. Sending into a list that hasn't been cleaned in a year typically produces a 5-15% hard bounce rate, well past SES's 5% review threshold and 10% suspension threshold. Aggregate bounce events into a rolling 24-hour rate per identity and page on >2%. The cheapest implementation is a CloudWatch metric filter on your Lambda logs.

Retry queue for transient bounces. Transient bounces with bounceSubType: MailboxFull or ContentRejected are sometimes recoverable. Don't suppress these forever; instead, mark them with a backoff timestamp and re-attempt after 24-72 hours. Permanent General bounces never come back - suppress them permanently.

Typical SES bounce subtype distribution Bounce subtype distribution (typical production account) Share of all bounce notifications over a 30-day window Permanent / General Permanent / Suppressed Transient / MailboxFull Transient / General Transient / ContentRejected Undetermined 25% 50% 75% 100% 62% 14% 11% 7% 4% 2%
Illustrative breakdown based on aggregate patterns observed across BYO-SES accounts. Disclosure: Mailblast operates a hosted layer on customers' own SES; figures are directional, not from a single account.

Troubleshooting

When notifications stop arriving, the cause is almost always one of four things. Pending subscription confirmation is the most common - if you subscribed an HTTPS endpoint and it didn't respond to the initial SubscriptionConfirmation POST, the subscription sits in pending state forever. Check the subscription status with aws sns list-subscriptions-by-topic.

Topic policy missing the AWS:SourceArn condition - if you tightened the policy after creating it and locked yourself out, SES will silently fail to publish. Check CloudWatch metrics for the topic; NumberOfNotificationsFailed will spike. Wrong region - SNS topic and SES identity must be in the same Region; cross-region publish is not supported for identity notifications. KMS encryption without SES key access - the symptom is InvalidParameterValue at configuration time, fixed by adding kms:GenerateDataKey and kms:Decrypt to the key policy for ses.amazonaws.com.

Always attach a dead-letter queue to your subscription (or SQS redrive policy if you use SQS). If your handler throws on a payload shape it doesn't recognize, you want those events captured for replay, not dropped. SNS retries HTTPS subscriptions aggressively, but a bug that consistently throws will eventually exhaust retries and be lost without a DLQ.

Frequently asked questions

Do I have to use SNS, or can SES email feedback to me?

SES can forward bounces and complaints to the verified identity's email address by default, but that's only useful for very low volume. For any production workload you want machine-readable SNS notifications so your application can update suppression lists and metrics in real time. Once you point SES at an SNS topic, you should disable email feedback forwarding for that identity to avoid double notifications.

What's the difference between identity notifications and configuration set event destinations?

Identity notifications are configured per verified domain or email address and cover bounce, complaint, and delivery. Configuration set event destinations are attached per-message via the X-SES-CONFIGURATION-SET header and cover a wider set of events including send, reject, open, click, renderingFailure, and deliveryDelay. Most production systems use both: identity notifications for the safety net, configuration sets for engagement tracking.

Does SES retry failed SNS deliveries?

SNS itself retries delivery to subscribers according to its standard delivery policy (over 100,000 attempts for HTTPS endpoints over multiple days). However, if the SNS topic itself is deleted or SES loses permission to publish, SES will remove the topic configuration for bounce or complaint notifications and re-enable email feedback forwarding. Always attach a dead-letter queue to critical SNS subscriptions.

Will SES still suppress hard bounces if I don't subscribe to the SNS feedback loop?

Yes. The SES account-level suppression list operates independently of SNS notifications. SES automatically adds addresses that produce hard bounces or complaints to the suppression list and silently drops future sends to them. SNS notifications are how your application learns about it so you can update your own subscriber list, not how suppression itself works.

Can one SNS topic receive feedback from multiple SES identities?

Yes, and it's a common pattern. The topic access policy can list multiple SES identity ARNs in the AWS:SourceArn condition, or you can use a wildcard. Each notification includes the source identity in the mail.sourceArn field so your subscriber can route by sender. Just remember the topic and the identities must be in the same AWS Region.

Ready to Start Your Email Marketing Journey?

Join thousands of businesses using Mailblast to grow their audience.

← Back to Blog