One Missed Event, One Suspended Account
Amazon SES is infrastructure-level email. You get low per-message costs, high throughput, and direct access to AWS IP reputation. What you do not get, by default, is any meaningful visibility into what happens after you press send. SES records every bounce, complaint, delivery, open, and click, and it will publish those events to the destination of your choice, but only if you configure it to do so. That configuration does not come with a dashboard, does not come with alerting, and does not come with any automatic action on your sending behaviour. All of that work is left to you.
The stakes are not abstract. AWS enforces hard thresholds on bounce and complaint rates. A bounce rate above 5% triggers a review; above 10%, AWS may pause your account's ability to send. For complaints, the recommended ceiling is 0.1%, and AWS will pause sending when your complaint rate exceeds 0.01%. On a list of 10,000 addresses, that 0.1% recommended ceiling amounts to roughly 10 spam complaints before you enter monitored territory. Miss those signals and you will not find out you have a problem until a customer files a support ticket or a developer is pulling logs at midnight. By then, the damage to your sender reputation may already be done.
This article explains how to wire up Amazon SES event destinations correctly, what to do with the data in real time, and where the gaps are that raw event streaming alone cannot fill.
What Are Amazon SES Event Destinations?
An event destination is the AWS service to which Amazon SES publishes the email sending events associated with a configuration set. Configuration sets are the glue layer: a named group of rules you attach to outgoing email by including the X-SES-CONFIGURATION-SET header or by specifying the configuration set name in the API call. Every message sent with a configuration set attached will have its events routed to whatever destinations you have registered against that set.
This matters because SES does not capture events globally. If a message is sent without a configuration set, no event data is published to your destinations at all. That single omission, forgetting to assign a default configuration set to your sending identity, is one of the most common causes of invisible bounce accumulation in production systems.
Configuration sets support multiple event destinations simultaneously. You can send bounce and complaint events to SNS for real-time Lambda processing while simultaneously streaming all event types to Amazon Data Firehose for long-term storage. The two destinations operate independently.
SES Event Types and Why Each One Matters
Send
A Send event fires when SES accepts the message and begins attempting delivery to the recipient's mail server. It is the baseline for calculating your send volume. On its own it carries no reputation risk, but it anchors your rate calculations: without an accurate send count, your bounce and complaint percentages are meaningless.
Delivery
A Delivery event confirms that the receiving mail server accepted the message. It does not guarantee inbox placement, but it does confirm successful handoff. Tracking delivery rates alongside send rates lets you spot silent drops, cases where messages are accepted by SES but quietly rejected by the downstream MX server before a bounce notification is generated.
Bounce (Hard and Soft)
Bounce is the most operationally critical event type. SES classifies bounces as Permanent (hard) or Transient (soft). A Permanent bounce means the address does not exist, the domain does not exist, or the receiving server has permanently rejected the message. You should remove that address from your lists immediately and never send to it again. A Transient bounce indicates a temporary failure such as a full mailbox, and SES will attempt redelivery for a period before giving up.
Only hard bounces count against your AWS bounce rate metric. Soft bounces do not feed into the threshold calculation, but they do indicate list quality problems that can worsen over time. Each bounce notification also carries a bounce subtype, with values such as NoEmail, MailboxFull, MessageTooLarge, and ContentRejected, which tell you the specific reason for the failure. Many implementations handle only the top-level bounce type and discard the subtype entirely, losing actionable diagnostic information in the process.
Complaint
A Complaint event fires when a recipient clicks the spam button in their mail client and the ISP forwards that signal to AWS via a feedback loop. Amazon SES maintains complaint feedback loops with the major email providers so you do not have to negotiate them individually. A complaint is a stronger negative signal than a bounce: a bounce means the address was wrong, but a complaint means a real person actively decided your email was unwanted. Every unhandled complaint nudges your complaint rate closer to the thresholds that trigger account review or suspension.
Open and Click
Open and Click events are engagement signals. SES tracks opens by injecting a 1x1 pixel tracking image and tracks clicks by rewriting links through a redirect. These events feed engagement-based segmentation: recipients who consistently open and click should be treated differently from those who have not engaged in months. Sending repeatedly to disengaged recipients is a predictable path to elevated complaint rates. Suppressing non-engagers before they become complainers is far cheaper than recovering from a complaint spike.
Choosing Your Destination: Trade-offs at a Glance
SES API v2 supports SNS, CloudWatch, Amazon Data Firehose, and EventBridge as native event destinations. SQS is not a direct SES destination but is reached via SNS fan-out, which is the most common production pattern. Each option has a different latency, durability, and processing model.
SNS is a pub/sub notification service that pushes events to all subscribers simultaneously. It is the right choice when you need immediate, fan-out delivery: a single SNS topic can trigger a Lambda function for real-time suppression, forward to an SQS queue for durable processing, and post to an HTTP endpoint for a third-party webhook, all from the same publish action. Delivery is push-based and near-instant, but SNS offers no built-in persistence if a subscriber is unavailable.
SQS via SNS fan-out is the right choice when you need guaranteed processing with backpressure support. Messages wait in the queue until a consumer retrieves them, making the architecture resilient to downstream processing outages. If your Lambda function is throttled or your processing service restarts, events accumulate safely and are worked through when capacity returns. The standard retention period is four days, extendable to 14. The SNS-to-SQS fan-out pattern is the most common production architecture for SES event handling precisely because it combines push delivery with durable buffering.
CloudWatch as an event destination does not receive the raw event payload. Instead, SES publishes numeric metrics, such as counts of sends, bounces, complaints, and deliveries, sliced by the CloudWatch dimensions you define on the configuration set. This is the right destination for dashboards and threshold alarms rather than per-message processing. The built-in Reputation.BounceRate and Reputation.ComplaintRate metrics in the AWS/SES namespace are automatically published to CloudWatch regardless of configuration sets, giving you account-wide alarm coverage even without custom dimensions.
Amazon Data Firehose is the right destination for high-volume archival. It buffers events and delivers them in batches to S3, Redshift, or OpenSearch. Under high sending volumes, SNS can become expensive if you are processing millions of events daily purely for storage purposes. Firehose is cost-effective for the write-once-query-later pattern and integrates naturally with Athena for ad hoc analysis of historical event data.
EventBridge is a serverless event bus with content-based routing rules. Where SNS delivers every event to every subscriber, EventBridge lets you write filter rules against the event payload so that, for example, only Permanent bounce events route to your suppression Lambda while all events route to your audit trail. EventBridge also supports event archiving and replay, which is useful when you need to reprocess historical events after a bug in your handler. It is a more complex setup than SNS but provides considerably more flexibility for sophisticated routing requirements.
Setting Up a Configuration Set and SNS Event Destination
Via the AWS Console
Navigate to Amazon SES in the AWS console, select Configuration sets from the left-hand menu, and choose Create configuration set. Give it a descriptive name such as transactional-prod. Once the set is created, open it and choose the Event destinations tab, then select Add destination. Choose the event types you want to capture, select Amazon SNS as the destination type, and specify or create the target SNS topic ARN. Save the destination. Finally, assign the configuration set to your sending identity under Verified identities as the default configuration set so that every outgoing message is automatically tagged.
Via the AWS CLI
The following commands create a configuration set and attach an SNS destination using SES API v2:
aws sesv2 create-configuration-set \
--configuration-set-name transactional-prod \
--region eu-west-1
aws sesv2 create-configuration-set-event-destination \
--configuration-set-name transactional-prod \
--event-destination-name bounce-complaint-sns \
--event-destination '{
"Enabled": true,
"MatchingEventTypes": ["BOUNCE","COMPLAINT","DELIVERY","SEND"],
"SnsDestination": {
"TopicArn": "arn:aws:sns:eu-west-1:123456789012:ses-events"
}
}' \
--region eu-west-1
To set the configuration set as the default for a sending identity, update the identity's configuration:
aws sesv2 put-email-identity-configuration-set-attributes \
--email-identity your-domain.com \
--configuration-set-name transactional-prod \
--region eu-west-1
Ensure the SNS topic policy grants SES permission to publish. Without the correct resource-based policy on the topic, events will be silently dropped with no error surfaced in the SES console.
Processing Bounce Events in Real Time: Lambda Handler
SES publishes bounce, complaint, and delivery notifications to SNS as JSON payloads. The top-level JSON object contains a notificationType string and a corresponding nested object. When using configuration sets with event publishing, the field is named eventType rather than notificationType. Your handler must account for both field names if you mix identity-level SNS notifications with configuration set event destinations. SES does not guarantee ordering or batching of notifications, so your code must handle both single-recipient and multi-recipient payloads in the same bounce object.
The following Python Lambda handler receives SNS-triggered events, identifies hard bounces, and adds the offending addresses to the SES account-level suppression list:
import json
import boto3
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
sesv2 = boto3.client('sesv2')
def lambda_handler(event, context):
for record in event['Records']:
# SNS wraps the SES notification inside record['Sns']['Message']
message = json.loads(record['Sns']['Message'])
# Support both identity-level and configuration-set event payloads
notification_type = message.get('notificationType') or message.get('eventType', '')
if notification_type == 'Bounce':
handle_bounce(message)
elif notification_type == 'Complaint':
handle_complaint(message)
def handle_bounce(message):
bounce = message.get('bounce', {})
bounce_type = bounce.get('bounceType') # 'Permanent' or 'Transient'
bounce_subtype = bounce.get('bounceSubType') # e.g. 'NoEmail', 'MailboxFull'
for recipient in bounce.get('bouncedRecipients', []):
email = recipient.get('emailAddress', '')
if not email:
continue
if bounce_type == 'Permanent':
logger.info(f"Hard bounce ({bounce_subtype}): suppressing {email}")
suppress_address(email, 'BOUNCE')
else:
# Soft bounce: log for trend monitoring but do not suppress immediately
logger.info(f"Soft bounce ({bounce_subtype}) for {email}, no immediate suppression")
def handle_complaint(message):
complaint = message.get('complaint', {})
feedback_type = complaint.get('complaintFeedbackType', 'unknown')
for recipient in complaint.get('complainedRecipients', []):
email = recipient.get('emailAddress', '')
if not email:
continue
logger.info(f"Complaint (feedback type: {feedback_type}): suppressing {email}")
suppress_address(email, 'COMPLAINT')
def suppress_address(email, reason):
try:
sesv2.put_suppressed_destination(
EmailAddress=email,
Reason=reason # 'BOUNCE' or 'COMPLAINT'
)
logger.info(f"Added {email} to account suppression list, reason: {reason}")
except Exception as e:
logger.error(f"Failed to suppress {email}: {e}")
raise
A few implementation details deserve attention. First, put_suppressed_destination adds the address to the SES account-level suppression list, which means SES will silently skip future sends to that address without generating a new bounce event. This protects your bounce rate automatically. Second, complaints use the same suppression list mechanism; adding a complainant prevents future sends and satisfies the requirement most ISPs and mailbox providers expect of responsible senders. Third, the complaintFeedbackType field carries values such as abuse, fraud, virus, and other. An abuse complaint warrants not only suppression but also an internal alert, since it may indicate a compromised sending flow or a list segment that was acquired improperly.
CloudWatch Metrics, Dashboards, and Alarms
SES automatically publishes two reputation metrics to CloudWatch in the AWS/SES namespace: Reputation.BounceRate and Reputation.ComplaintRate. These are the same rolling rates that AWS uses internally to determine whether to place your account under review. Unlike the raw event counts you build from your own Lambda handlers, these metrics reflect AWS's own calculation methodology, which uses a representative volume rather than a fixed time window to avoid distortion from low-volume sending periods.
A sensible early-warning alarm configuration sets the bounce rate alert at 5% (the review threshold) with a secondary alert at 2.5% to give you earlier notice, and the complaint rate alert at 0.08%, well below the 0.1% recommended ceiling. The following Terraform snippet illustrates the pattern:
resource "aws_cloudwatch_metric_alarm" "ses_bounce_rate" {
alarm_name = "ses-bounce-rate-warning"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "Reputation.BounceRate"
namespace = "AWS/SES"
period = 3600
statistic = "Maximum"
threshold = 0.025
alarm_actions = [aws_sns_topic.alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "ses_complaint_rate" {
alarm_name = "ses-complaint-rate-warning"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "Reputation.ComplaintRate"
namespace = "AWS/SES"
period = 3600
statistic = "Average"
threshold = 0.0008
alarm_actions = [aws_sns_topic.alerts.arn]
}
These alarms give your team time to investigate and remediate before AWS intervenes. A complaint rate climbing from 0.02% to 0.08% over 48 hours is a signal you can act on; a complaint rate that crosses 0.1% and enters monitored territory without warning is a problem that was already avoidable.
Common Pitfalls
No default configuration set assigned. If you send a message without specifying a configuration set, SES processes the send but publishes no events to your destinations. A single deployment that omits the configuration set header on a high-volume batch can produce thousands of untracked bounces. Always assign a default configuration set to your sending identity so that untagged sends are captured automatically.
Missing SNS topic permissions. The SNS topic must have a resource-based policy that explicitly grants sns:Publish to the SES service principal (ses.amazonaws.com). Without this, SES silently fails to publish events. There is no error raised in the SES console; events simply disappear. Test your setup using the SES mailbox simulator addresses before going to production.
Not handling all bounce subtypes. Many production handlers check only for bounceType == 'Permanent' and ignore the subtype entirely. A ContentRejected subtype, for instance, suggests your email content is being flagged as spam by a receiving server, which is a different problem from a NoEmail subtype and requires a different remediation path. Log subtypes and alert on unexpected ones.
Losing events under high volume. At very high sending volumes, an SNS topic with a synchronous Lambda trigger can hit Lambda concurrency limits, causing events to be dropped if no dead-letter queue is configured. The safer pattern is SNS to SQS, with Lambda consuming from the queue. If the Lambda is throttled, messages accumulate in SQS rather than being lost, and you can process the backlog once capacity is restored.
Treating soft bounces as harmless. Soft bounces do not count against your AWS bounce rate, but a persistent pattern of soft bounces to the same addresses is often a precursor to those addresses hardening into permanent failures. Track soft bounce frequency per address and implement progressive suppression for chronic soft bouncers.
Monitoring Gaps That Event Destinations Alone Cannot Fill
Raw event destinations are a stream of individual events. They are not a monitoring system. Once you have wired up SNS, SQS, and CloudWatch alarms, you have the building blocks, but several critical visibility gaps remain.
There is no aggregate trend view across sending identities. If you operate multiple domains or subdomains through SES, each has its own reputation metrics. A configuration set in one region feeding a CloudWatch alarm gives you no visibility into what is happening on a sending identity in another region or under a different configuration set. Spotting a cross-identity pattern requires manual correlation of multiple CloudWatch namespaces.
There is no out-of-the-box historical trend chart that shows your bounce rate trajectory over weeks or months. CloudWatch retains high-resolution metrics for limited periods, and the default SES reputation metrics have coarse retention. Understanding whether your bounce rate is trending upward over a 30-day horizon, the kind of view that informs list hygiene decisions, requires custom tooling to build and maintain.
There is no alerting that maps your current metrics to the specific AWS thresholds that trigger account review. You can build CloudWatch alarms at arbitrary thresholds, but you have to know what those thresholds are, keep them updated if AWS changes its policy, and ensure you have alarms in place for every sending identity and region you operate.
Where SES Monitor Fills the Gaps
SES Monitor is purpose-built to address these visibility gaps. Rather than replacing event destinations, it sits alongside them, ingesting your SES event data and adding the aggregation, trend analysis, and threshold-aware alerting layer that raw event streaming does not provide.
The aggregated dashboard gives you a single view across all your sending identities and configuration sets, showing bounce rates, complaint rates, and delivery rates over selectable time windows. You can see at a glance whether a rate that looks acceptable today is drifting upward over the past two weeks, which is the kind of trend that raw CloudWatch graphs do not surface without significant custom dashboard work.
Threshold alerts in SES Monitor are calibrated against the actual AWS review and suspension thresholds, so you receive warnings at meaningful fractions of those limits rather than having to derive appropriate values yourself. Alerts fire before you are at risk, giving you time to investigate root causes rather than filing an urgent support case after the fact.
For teams that need to forward SES bounce and complaint signals to external systems such as a CRM, a marketing platform, or a customer data platform, SES Monitor provides webhook forwarding so that suppression and unsubscribe signals propagate to every system that sends email on your behalf, not just to your AWS Lambda handlers.
Historical data retention goes beyond what CloudWatch retains natively, so you can audit your sending health over months, identify seasonal patterns, and demonstrate compliance with best-practice thresholds to stakeholders.
Conclusion
Amazon SES event destinations are not optional infrastructure. They are the mechanism by which you find out about every bounce, every complaint, and every delivery failure in time to act on it. Getting the setup right means assigning a default configuration set to every sending identity, granting the correct SNS topic permissions, handling all bounce types and subtypes in your processing code, and protecting against event loss under high volume with an SNS-to-SQS fan-out pattern. Layer CloudWatch alarms calibrated well below the AWS review thresholds on top of that, and you have genuine protection against the gradual reputation erosion that ends in account suspension. Add an aggregated monitoring layer that shows trend data across all your sending identities, and you move from reactive firefighting to proactive reputation management.