Guide

SES Bounce Rate: AWS Thresholds, Calculation, and How to Fix It

Learn how your SES bounce rate is calculated, the exact AWS thresholds (2%, 5%, and 10%), and how to fix a high bounce rate before AWS pauses your account.

Kai Tanaka

Kai Tanaka


Your SES bounce rate is the percentage of sent emails that Amazon SES could not deliver to the recipient's inbox. To maintain a healthy reputation on Amazon Web Services, you must keep your bounce rate below 2%. If your SES bounce rate exceeds 5%, AWS will place your account under review, and if it reaches 10%, AWS may pause your sending capabilities entirely.

Understanding how this metric is calculated and how to manage your suppression list is critical to keeping your sending active — especially when migrating away from traditional mailer services to own your infrastructure. If you are reading this, one of three things is true:

  1. AWS just sent you an email telling you your account is under review because of bounces or complaints. You're trying to figure out what happens next and how fast.
  2. AWS has already paused your sending. Production is broken. You opened a support case and you don't know what to put in it.
  3. Your reputation metrics are creeping in the wrong direction and you want to fix it before either of the above happens.

This post is the runbook for all three. It covers the exact thresholds AWS uses, how to check where you actually stand, a triage playbook for the first hour after a review notice, the bounce- and complaint-handling pipelines you need so this never happens again, and the template for what to write back to AWS support.

It assumes you're already in production SES (out of the sandbox) and you have IAM credentials with sesv2:* permissions on the account in question.

The thresholds AWS actually uses

There are two metrics AWS watches: bounce rate and complaint rate. Each has three states.

MetricHealthyUnder reviewSending pause
Bounce rate< 5% (AWS recommends < 2%)≥ 5%≥ 10%
Complaint rate< 0.1%≥ 0.1%≥ 0.5%

You will see contradictory numbers on the open web — particularly a re:Post knowledge base article that says the complaint-rate pause threshold is 0.01%. The authoritative source is the SES Reputation metrics messages page in the SES Developer Guide, which states the review threshold is 0.1% and the pause threshold is 0.5%. Use those.

A few things to know about how these numbers behave in practice:

The rate is calculated over a representative volume, not a fixed time window. AWS does not say "your bounce rate over the last 24 hours". It picks a volume of email that "represents your typical sending practices" — different for every account, and it shifts as your sending patterns change. The practical implication: a low-volume sender who sends one bad batch can see their reported bounce rate stay elevated for days, because the representative window is large relative to their daily send. You cannot wait for "tomorrow" for a bad bounce rate to wash out.

The account-level suppression list does not count. Messages dropped to addresses on your own account-level suppression list don't go into Reputation.BounceRate or Reputation.ComplaintRate. They're counted in the absolute Bounce / Complaint metrics, but those are not the metrics AWS enforces against. This is the lever the rest of the post is built around.

The global suppression list does count. SES maintains an internal global suppression list across all customers. If you send to an address on it, that's a bounce that counts against your reputation. You can override it for specific addresses with your own account-level list, but you can't see or query the global one.

How AWS calculates your SES bounce rate

AWS calculates your SES bounce rate over a representative volume of recent email — not a simple lifetime average, and not a fixed time window. The calculation is:

Bounce Rate = (Hard Bounces / Total Attempted Deliveries) * 100

Only hard bounces (permanent delivery failures like a non-existent mailbox or a typo in the address) count toward this rate. Soft bounces (temporary failures like a full mailbox) do not. If your sending volume drops suddenly but hard bounces remain in the pipeline, your percentage will spike.

The calculation also depends on proper DNS authentication. If your domain is missing a correct SPF record, DMARC record, or DKIM signature, receiving mail servers may reject your mail outright — and those rejections count as hard bounces, spiking your rate.

First: find out where you actually stand

Before you do anything else, get the real numbers. The console hides the precision you want.

# Current bounce rate, complaint rate, and account status
aws sesv2 get-account --query 'SendQuota'
aws sesv2 get-account --query 'EnforcementStatus'

# Or pull the raw CloudWatch metrics
aws cloudwatch get-metric-statistics \
  --namespace AWS/SES \
  --metric-name Reputation.BounceRate \
  --start-time $(date -u -v-7d +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Maximum

aws cloudwatch get-metric-statistics \
  --namespace AWS/SES \
  --metric-name Reputation.ComplaintRate \
  --start-time $(date -u -v-7d +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Maximum

EnforcementStatus returns one of: HEALTHY, PROBATION, SHUTDOWN. If it's PROBATION or SHUTDOWN, the next steps matter a lot.

If you have multiple configuration sets, also pull the per-config-set reputation metrics. A single noisy product line can drag the whole account into review while the rest of your sending is fine:

aws sesv2 get-configuration-set-event-destinations \
  --configuration-set-name <your-config-set>

The first-hour triage playbook

You got the review email. Production is still sending (or has just been paused). What to do, in order.

1. Stop sending to your riskiest segments

The first thing AWS reads when you reply to a support case is whether you took action. If you keep firing the same payload that caused the problem, the reply gets ignored.

If you have any segment of your sending that you suspect is the source — re-engagement, scraped lists, anything older than 12 months, anything you imported from another provider — pause it from your application. Not at the SES layer, at the queue/job layer. You want the volume of messages going into SES to drop measurably, ideally within the next 30 minutes.

This is the single most underrated step. AWS support cases that say "we are investigating" but show the same outbound volume get dismissed.

2. Enable account-level suppression for both reasons

If your SES account was created after November 25, 2019, this is already on. If older, or if you're not sure, run it. It's idempotent.

aws sesv2 put-account-suppression-attributes \
  --suppressed-reasons BOUNCE COMPLAINT

From this point on, any address that bounces (permanent) or complains gets added to your account suppression list automatically. SES will refuse to send to it. Messages dropped this way do not count against your reputation. That's the entire point.

Verify:

aws sesv2 get-account --query 'SuppressionAttributes'

You should see "SuppressedReasons": ["BOUNCE", "COMPLAINT"].

3. Bulk-suppress everything you already know is bad

You almost certainly have data in your own database showing bounces and complaints from the past — bounce notification emails, SNS notifications you logged, support tickets, unsubscribes. Pull all of them and load them into the SES suppression list. Even addresses that bounced six months ago should go in, because if any of them are still in your send list they're future bounces waiting to happen.

For under 100 addresses, you can use PutSuppressedDestination in a loop:

while read email; do
  aws sesv2 put-suppressed-destination \
    --email-address "$email" \
    --reason BOUNCE
done < known-bounces.txt

For more than 100 addresses, use the bulk import path — newline-delimited JSON to S3 plus a CreateImportJob:

# bounces.json (one record per line, newline-delimited JSON)
{"emailAddress":"a@example.com","emailIdentity":{"action":"PUT","reason":"BOUNCE"}}
{"emailAddress":"b@example.com","emailIdentity":{"action":"PUT","reason":"BOUNCE"}}

aws s3 cp bounces.json s3://your-bucket/bounces.json

aws sesv2 create-import-job \
  --import-destination 'SuppressionListDestination={SuppressionListImportAction=PUT}' \
  --import-data-source 'S3Url=s3://your-bucket/bounces.json,DataFormat=JSON'

Cap per import: 100,000 addresses. Concurrent jobs: 20.

4. Audit your bounce stream for the last 30–90 days

If you're already capturing bounce/complaint notifications via SNS (you should be — see the next section), aggregate them. You're looking for two things:

  • Concentration. Are 80% of your bounces coming from one configuration set, one campaign, one signup source? That's your culprit.
  • Type breakdown. SES classifies bounces into Permanent, Transient, and Undetermined. Only Permanent bounces are unambiguous deliverability failures. A spike in Permanent.General or Permanent.NoEmail means you have invalid addresses. A spike in Transient.MailboxFull is recoverable. A spike in Permanent.Suppressed means you're hitting the global suppression list — you need to stop sending to those addresses regardless.

If you don't have an SNS pipeline yet, the bounce headers in the bounce notification emails AWS sends to your account contact will tell you the same thing. Slower, but it's what you have.

Why your rate spiked (in rough order of frequency)

I'm not going to give you a generic "best practices" list. Here's what actually causes bounce and complaint rates to spike, ranked by how often I see each one as the root cause:

  1. You sent to an old list. Email addresses decay at roughly 25% per year. A list collected three years ago will have 50%+ invalid addresses. Re-engaging old lists is the #1 reason SES accounts get reviewed.
  2. You imported a list from a previous provider. SendGrid/Mailgun/Postmark suppression lists do not come with you. If you migrated and didn't import suppressions, the addresses they were silently filtering for you are now bouncing on SES.
  3. No double opt-in. Someone typed jonh@gmial.com into your signup form. That's a hard bounce on every email you ever send them.
  4. You're not honoring unsubscribes fast enough. If "unsubscribe" takes more than one click, users hit the "spam" button instead. That's a complaint, not an unsubscribe. Complaints are 50× worse than unsubscribes for your reputation.
  5. You mixed transactional and marketing on the same identity. Marketing complaints poison transactional deliverability. Send them on separate configuration sets at minimum, separate identities ideally.
  6. You changed your From address or domain. Mailbox providers treat new sending identities with suspicion. A new domain with no warmup sending at production volume looks exactly like a spammer to Gmail.
  7. Spam-trap hits. If an address used to be real and was repurposed by Gmail/Microsoft as a spam trap, sending to it generates an invisible complaint. You only find out about spam-trap hits via DMARC aggregate reports or sudden, unexplained reputation drops.

Almost every account I've seen go into review fits one of (1)–(4).

Build a bounce-handling pipeline that actually works

Account-level suppression is the safety net. The pipeline below is what stops the problem at the application layer, before the safety net has to catch it — it's the same plumbing you'd otherwise have to build to run your own email marketing platform from scratch on SES.

Architecture

SES (SendEmail)
    │
    ▼ (configuration set with event destination)
SNS topic
    │
    ▼
SQS queue (for buffering + retry)
    │
    ▼
Lambda (parse + suppress + record)
    │
    ▼
Your application DB (suppression table)

The SQS step is non-negotiable. Without it, a Lambda failure during a bounce burst loses the events forever.

SNS notification payload (what you actually parse)

{
  "notificationType": "Bounce",
  "bounce": {
    "bounceType": "Permanent",
    "bounceSubType": "General",
    "bouncedRecipients": [
      {
        "emailAddress": "recipient@example.com",
        "action": "failed",
        "status": "5.1.1",
        "diagnosticCode": "smtp; 550 5.1.1 The email account that you tried to reach does not exist."
      }
    ],
    "timestamp": "2026-05-13T10:00:00.000Z",
    "feedbackId": "01000189..."
  },
  "mail": { "messageId": "...", "source": "you@example.com" }
}

The decision logic:

bounceTypeWhat to do
PermanentAdd the address to your application suppression table immediately. Also call PutSuppressedDestination so SES drops future sends before they count against you.
TransientTrack count and last-bounce time. After N transient bounces in M days, suppress. (Reasonable starting point: 3 in 14.)
UndeterminedTrack for visibility but don't auto-suppress. Review weekly.

Within Permanent, the subtype matters:

  • General — invalid address. Suppress, no retry.
  • NoEmail — the address has never existed. Suppress, but also audit your signup flow — these shouldn't be making it into your list.
  • Suppressed — the address was on the SES global suppression list. Suppress and stop trying.
  • OnAccountSuppressionList — your own list caught it. No further action needed, but if you're seeing these in volume your application is not checking suppressions before sending. Fix that.

Don't skip the application-level suppression table

The SES account suppression list is a safety net at the API boundary. By the time SES is dropping your message, you've already wasted the work of templating it, looking up the recipient, and making the API call. Your application should refuse to send before it ever gets to SES.

A minimal schema:

CREATE TABLE email_suppressions (
  email TEXT PRIMARY KEY,
  reason TEXT NOT NULL,         -- 'bounce' | 'complaint' | 'manual'
  bounce_subtype TEXT,           -- nullable
  suppressed_at TIMESTAMP NOT NULL DEFAULT NOW(),
  source TEXT                    -- which campaign/config set caused it
);

CREATE INDEX idx_suppressions_suppressed_at ON email_suppressions (suppressed_at);

Check this table before every send. It's a single index lookup and it saves you from your own mistakes.

Build a complaint-handling pipeline (same shape, different urgency)

Complaints are about 5× more reputation-damaging than bounces at the same rate (which is why the threshold is so much lower — 0.1% vs 5%). The pipeline is the same shape as bounces, but:

  • Suppress on the first complaint, always. There is no "transient" complaint. If a recipient clicked "spam", you do not get a second chance with them.
  • Investigate every complaint individually for the first 100. Look at which campaign, which template, which segment. Complaint patterns tell you exactly which content is over the line.
  • Audit your unsubscribe flow. Most complaint spikes are users who couldn't find or didn't trust your unsubscribe link.

A complaint notification looks like:

{
  "notificationType": "Complaint",
  "complaint": {
    "complainedRecipients": [{"emailAddress": "recipient@example.com"}],
    "timestamp": "2026-05-13T10:00:00.000Z",
    "feedbackId": "01000189...",
    "complaintFeedbackType": "abuse"
  }
}

Note: only ISPs that participate in feedback loops report complaints back to SES. Gmail famously does not. That means your true complaint rate is higher than what SES shows — possibly by a factor of 2–3. Treat the reported number as a floor, not a measurement.

What to send to AWS support when responding to a review

If your account is under review or paused, the only thing that changes the outcome is the reply you put into the support case. AWS support does not respond well to "please reinstate us, we'll be more careful". They respond to specific, technical evidence of what changed.

A working template:

Hi,
Thank you for flagging the elevated [bounce / complaint] rate on our account. We have completed our investigation and taken the following corrective actions.
Root cause. [One specific sentence. Example: "On 2026-05-09 we imported a list of 18,400 addresses from a previous provider without migrating their suppression list, which caused a spike in permanent bounces against addresses that provider had previously suppressed."]
What we changed:
  1. We added all 14,200 previously suppressed addresses from the prior provider to our SES account-level suppression list via CreateImportJob on 2026-05-12. Import job ID: [id].
  2. We enabled account-level suppression for both BOUNCE and COMPLAINT reasons via PutAccountSuppressionAttributes.
  3. We deployed an SNS → SQS → Lambda pipeline on 2026-05-12 that adds every permanent bounce and every complaint to both the SES suppression list and our application suppression table, blocking future sends before they reach the SES API.
  4. We added a CloudWatch alarm at 3% bounce rate and 0.05% complaint rate that pages our on-call.
  5. We paused our re-engagement campaign and will not resume it until we have implemented double opt-in confirmation.
Current state. Bounce rate over the last 24 hours of sending: [X%]. Complaint rate: [Y%]. We sent [N] messages over that period.
Prevention. [One concrete sentence on the process change. Example: "All list imports now go through a mandatory review checklist that includes suppression-list migration; the checklist is enforced by a pre-merge GitHub Action on our customer-data repository."]
Please reinstate sending when convenient.

Three things make this work:

  1. A specific root cause. Not "we had quality issues". A sentence that names a date, a number, and a specific operational mistake.
  2. Verifiable evidence. Job IDs, alarm names, dates. AWS reviewers can confirm these from their side. Hand-waving cannot be confirmed.
  3. A prevention mechanism that is not "we will be more careful". A code change, a checklist enforced by automation, a runbook.

The number of accounts I've seen reinstated within hours of sending a reply that looks like the above is high. The number reinstated after sending "please help, we will do better" is approximately zero.

What to monitor so this doesn't happen again

AWS recommends CloudWatch alarms at 5% bounce / 0.1% complaint. Don't use those. Those are the AWS review thresholds. By the time you're alarmed, you're already in the danger zone.

Set your own alarms at roughly half the review threshold so you have warning:

# Bounce rate alarm at 3%
aws cloudwatch put-metric-alarm \
  --alarm-name "ses-bounce-rate-warning" \
  --alarm-description "SES bounce rate approaching review threshold" \
  --namespace AWS/SES \
  --metric-name Reputation.BounceRate \
  --statistic Maximum \
  --period 3600 \
  --evaluation-periods 2 \
  --threshold 0.03 \
  --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ses-alarms

# Complaint rate alarm at 0.05%
aws cloudwatch put-metric-alarm \
  --alarm-name "ses-complaint-rate-warning" \
  --alarm-description "SES complaint rate approaching review threshold" \
  --namespace AWS/SES \
  --metric-name Reputation.ComplaintRate \
  --statistic Maximum \
  --period 3600 \
  --evaluation-periods 2 \
  --threshold 0.0005 \
  --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ses-alarms

Two evaluation periods at 1 hour each gives you a 2-hour smoothing window — enough to avoid spurious pages from a single bad batch, short enough to catch a trend.

Per configuration set, set the same alarms with the ses:configuration-set dimension. A noisy product line is usually a configuration-set-level problem, and you want it visible before it pollutes the account number.

Keep your SES bounce rate safe with SendOps

Instead of building your own complex pipelines to parse CloudWatch metrics and SNS topics, you can use SendOps.

SendOps is a full email marketing and operations platform that runs on your own Amazon SES account. It installs directly into your AWS infrastructure using CloudFormation and short-lived, secure STS credentials.

By running on top of your existing SES setup, SendOps protects your sender reputation automatically:

  • Automated Suppression Sync: Instantly flags and suppresses hard bounces and spam complaints so you never email them twice.
  • Proactive Alerting: Sets up half-threshold alarms to notify your team before AWS triggers a review.
  • Complete Email Operations: Manage contacts, segments, React Email templates, and workflows directly from a marketer-friendly interface, without paying a contact tax or per-email markups.

Learn more about SendOps

Further reading

If you're staring at a review email right now: do the four numbered steps under "first-hour triage" first, then write the support reply. Everything else can wait until tomorrow.