Absorb the burst, send at the quota
Amazon SES queued sending — SQS + Lambda rate limiting for burst and multi-tenant email
Your application produces email in spikes. Amazon SES accepts it at a fixed per-second rate. Put a queue between the two, size the consumer to the quota, and keep one tenant from spending the sending reputation every other tenant depends on.
Last updated: October 24, 2026Author: FactualMinds AWS ArchitectsReviewed by: Palaniappan P · AWS Solutions Architect — Professional
Problem
Amazon SES enforces a per-second maximum send rate and a 24-hour quota per account per Region. When an application calls SendEmail directly from request handlers, a campaign launch, a password-reset storm, or a nightly digest job pushes the call rate past MaxSendRate, and SES rejects the excess with a ThrottlingException ("Maximum sending rate exceeded"). Teams then bolt on in-process retries that multiply the burst, drop messages when the process restarts, and give no single place to see what was not sent. In multi-tenant products the problem compounds: one tenant's bulk send consumes the shared rate, and one tenant's bad list pushes the account bounce rate toward the 5% review threshold for everyone.
Solution
Decouple producers from SES with an Amazon SQS queue. Producers enqueue a small send request (recipient, template name, template data, tenant). A Lambda function consumes the queue through an event source mapping whose maximum concurrency, batch size, and per-message pacing are sized so the aggregate call rate stays under the account MaxSendRate read from GetAccount. Throttling responses are treated as retriable: the message returns to the queue with backoff instead of being dropped. Messages that keep failing move to a dead-letter queue for inspection and redrive. Each tenant (or each mail stream) sends through its own configuration set or SES tenant, so events, reputation metrics, and suppression scope stay per tenant, and CloudWatch alarms on Reputation.BounceRate and Reputation.ComplaintRate page someone before AWS places the account under review.
AWS services in this pattern
| Service | Role |
|---|---|
| Amazon SES (SESv2 API) | Sends the email. GetAccount returns SendQuota (MaxSendRate per second, Max24HourSend); configuration sets carry event destinations and reputation metrics; SES tenants isolate reputation per customer in multi-tenant senders |
| Amazon SQS (standard or FIFO) + dead-letter queue | Durable buffer between producers and SES. Standard for highest throughput; FIFO when per-tenant ordering or exactly-once enqueue matters, with MessageGroupId set to the tenant. The DLQ captures messages that exhaust maxReceiveCount |
| AWS Lambda (SQS event source mapping) | Consumer. ScalingConfig MaximumConcurrency (2 to 1,000) caps how many invocations run at once, which is the main control for staying under MaxSendRate. Partial batch responses return only failed messages to the queue |
| Amazon CloudWatch | Alarms on SES Reputation.BounceRate and Reputation.ComplaintRate, on SQS ApproximateAgeOfOldestMessage (backlog), and on DLQ depth |
| Amazon EventBridge or Amazon SNS | Configuration-set event destinations for BOUNCE, COMPLAINT, DELIVERY, and REJECT events, feeding suppression handling and per-tenant reporting |
Architecture components
Enqueue, never send inline
Request handlers and batch jobs write one message per email (or per small recipient batch) to SQS and return immediately. Keep the payload small, a template name plus data, so each message stays inside one 64 KB SQS billing chunk. Use SendMessageBatch (up to 10 messages per call) from bulk producers.
Consumer sized to MaxSendRate
Read MaxSendRate from GetAccount at deploy time and on a schedule. Set the event source mapping MaximumConcurrency and batch size so that concurrency × emails per second per invocation stays below MaxSendRate with headroom (target 70 to 80%). Measure your own per-call latency; do not assume it. Prefer MaximumConcurrency over reserved concurrency, as AWS recommends, so Lambda stops polling instead of throttling.
Throttling is retriable
SES returns ThrottlingException with "Maximum sending rate exceeded" or "Daily message quota exceeded". Report those messages as batch item failures so they return to the queue after the visibility timeout; AWS guidance is to wait up to 10 minutes before retrying either throttling error. Permanent errors (unverified identity, suppressed address) are logged and deleted, not retried.
DLQ and redrive
A redrive policy with a modest maxReceiveCount moves poison messages to a DLQ. Operators fix the cause (bad template data, a paused configuration set) and use SQS DLQ redrive to move messages back to the source queue, for standard and FIFO queues alike.
Per-tenant isolation
One configuration set per tenant or mail stream (transactional vs marketing) keeps event destinations, reputation metrics, and dedicated IP pools separate. For SaaS senders, SES tenant management adds per-tenant reputation policies and suppression scope. With FIFO, MessageGroupId = tenant ID keeps one tenant's backlog from blocking another tenant's messages.
Reputation alarms
CloudWatch alarms on Reputation.BounceRate at 0.05 (5%) and Reputation.ComplaintRate at 0.001 (0.1%), the levels AWS recommends, plus an earlier warning alarm of your choosing. To pause only the offending stream, a per-configuration-set alarm publishes to SNS, and a Lambda function calls PutConfigurationSetSendingOptions (SESv2) with SendingEnabled=false for that configuration set.
Why this pattern
Amazon SES gives every account a per-second maximum send rate and a 24-hour quota in each Region. Application traffic does not arrive at a steady per-second rate. A campaign launch, a batch job, or a burst of sign-ups calls SendEmail faster than the quota allows, and SES rejects the overflow with a ThrottlingException. That error is retriable, but in-process retries make the burst worse and lose messages on restart.
The fix is to separate producing email from sending it. Producers enqueue; one consumer drains the queue at a rate you control. This is the same decoupling covered in our event-driven microservices pattern, applied to the one downstream system with a hard per-second limit.
Sizing the consumer to the quota
SES reports your limits through the SESv2 GetAccount API as SendQuota.MaxSendRate (emails per second) and SendQuota.Max24HourSend. The consumer’s aggregate rate is roughly:
aggregate rate ≈ MaximumConcurrency × (emails sent per invocation ÷ invocation duration in seconds)Pick MaximumConcurrency and batch size so the aggregate rate stays at 70–80% of MaxSendRate, measured with your own per-call latency. Re-read the quota on a schedule (an EventBridge Scheduler rule is enough), because quota increases otherwise go unused. Keep a separate queue, with its own reserved slice of the rate, for time-critical mail such as password resets and one-time passcodes.
Handling throttling and failures
| SES response | Retry? | What the consumer does |
|---|---|---|
ThrottlingException — Maximum sending rate exceeded | Yes | Return the message as a batch item failure so it comes back after the visibility timeout |
ThrottlingException — Daily message quota exceeded | Yes, later | Return the message; AWS guidance for either throttling error is to wait up to 10 minutes before retrying |
| Sending paused for the configuration set or account | Not until fixed | Return the message; it moves to the DLQ after maxReceiveCount and is redriven once sending resumes |
| Message rejected, identity not verified, or address suppressed | No | Log and delete; retrying cannot succeed |
Reputation alarms belong in the same stack
AWS places an account under review at a bounce rate of 5% or more and may pause sending at 10%. For complaints, review starts at 0.1% and sending may pause at 0.5%. SES publishes these as the CloudWatch metrics Reputation.BounceRate and Reputation.ComplaintRate (values are ratios: 0.05 means 5%). Alarm at the AWS-recommended values and at an earlier warning level, and wire configuration-set event destinations (EventBridge or SNS) to your suppression handler. The full alarm and deliverability setup is in our Amazon SES deliverability consulting page; dedicated IP ramp-up is covered in the SES dedicated IP warm-up guide.
When this pattern is overkill
If you send a few thousand transactional emails a day, far below your quota, a direct SDK call with the SDK’s built-in retry is fine. Add the queue when any of these hold: bursts reach your MaxSendRate, you send on behalf of multiple tenants or brands, or losing an email on a deploy or restart is unacceptable. If you are moving to SES from another provider, see our SES migration services.
Trade-offs
Pro
Bursts become backlog instead of errors. A 10x spike drains at the quota rate and nothing is lost when a consumer restarts.
Con
Email is no longer sent within the request. Time-critical mail (one-time passcodes, password resets) needs its own queue and its own share of MaxSendRate so a marketing backlog cannot delay it.
Pro
One knob, MaximumConcurrency, controls the send rate, and raising it after an SES quota increase is a config change, not a code change.
Con
MaximumConcurrency caps concurrency, not calls per second. If per-call latency drops, the rate rises. Pace inside the handler, or alarm on throttling counts, so a faster network does not push you over MaxSendRate.
Pro
FIFO with MessageGroupId per tenant gives fairness and ordering per tenant for free.
Con
FIFO queues default to 300 transactions per second per API action (3,000 messages per second with batching) unless high-throughput mode is enabled, and they cost more per request than standard queues. Use standard queues when ordering does not matter.
Pro
Per-tenant configuration sets make bounce and complaint rates attributable, so the fix is "suppress tenant X" rather than "pause the account".
Con
More configuration sets, alarms, and IAM conditions to manage in IaC. Below a handful of tenants, two configuration sets (transactional and marketing) are usually enough.
Cost notes
The queue and consumer are small next to SES itself. List rates (us-east-1, from the pricing modules behind our calculators, as of 2026-10-17): SQS standard $0.40 per million requests and FIFO $0.50 per million, with the first million requests free each month on standard queues; Lambda $0.20 per million requests and $0.0000133334 per GB-second on arm64, with 1 million requests and 400,000 GB-seconds free each month. Amazon SES a-la-carte outbound is $0.10 per 1,000 emails. Worked example with stated assumptions: 10 million emails per month, one SendMessage per email (10 million requests), consumed through the Lambda event source mapping in batches of 10 (about 1 million ReceiveMessage and 1 million DeleteMessage requests), is 12 million SQS requests. On a standard queue that is (12 − 1 free) × $0.40 = $4.40. Consumed in batches of 10 (1 million Lambda invocations) at 512 MB and an assumed 1 second per batch, Lambda is about $6.87 before the free tier. SES send is $1,000. The queue-and-consumer layer ($4.40 + $6.87 = $11.27) is about 1.1% of the SES bill; batching producers with SendMessageBatch cuts the SQS line further.
Related patterns
Event-driven microservices on AWS — EventBridge, Pipes, and the Outbox Pattern
Production event-driven architecture on AWS — EventBridge custom buses, EventBridge Pipes for the transactional outbox, SQS dead-letter queues, Step Functions for orchestration, and Lambda or Fargate workers. Decouple services without dual-writes.
Multi-Tenant SaaS on AWS — Pool, Silo, and Bridge
Production-ready multi-tenant architecture for SaaS on AWS. Covers tenant isolation models (pool, silo, bridge), per-tenant cost attribution, noisy-neighbor mitigation, and the trade-offs CTOs actually wrestle with at Series B and beyond.
Consulting engagements that deliver this pattern
Amazon SES Email Deliverability Consulting | FactualMinds
Amazon SES email consulting from an AWS Select Tier Partner. SPF/DKIM/DMARC setup, inbox placement optimization, sender reputation, and scalable email infrastructure.
SES Migration & Email Delivery Services | FactualMinds
SES migration consulting — move from SendGrid, Mailgun, Postmark, or SparkPost to Amazon SES. Deliverability, SPF/DKIM/DMARC, monitoring, compliance.
AWS Serverless Architecture & Lambda Consulting
Scalable, cost-efficient applications with AWS serverless — Lambda, API Gateway, DynamoDB, Step Functions. Consulting from an AWS Select Tier Partner.
Deep dives
Amazon SES Tenant Management: The Ultimate Enterprise Guide
SES tenants (GA Aug 2025) add $0.005/tenant/mo reputation isolation—when ISV platforms need them vs configuration sets alone, with IAM, pricing, and IaC.
Amazon SES Dedicated IP Warm-Up: Standard vs Managed (2026)
On 3 July 2025 AWS published a dedicated-IP ramp from 1,050 emails on day 1 to 21 million on day 40. Standard dedicated IPs auto-warm on a fixed 45-day percentage; managed dedicated IPs warm per ISP. Use the fast ramp only in the 40–60% engagement band.
AWS SES Marketing Automation with EventBridge and Step Functions (2026)
When to keep Customer.io/Loops vs build journeys with EventBridge + Step Functions + SES — stream isolation, RFC 8058, Pro for DIPs, and a first-party journey architecture checklist.
How to Build Reliable Queue Systems on AWS (SQS, Kafka, Redis)
SQS, MSK Kafka, and Redis queues are not interchangeable. Each has different cost models, ordering guarantees, and failure modes. This guide covers when to use each, how to autoscale workers on queue depth, and how to build idempotent consumers.
Frequently asked questions
How do I stop "Maximum sending rate exceeded" errors in Amazon SES?
Stop calling SES directly from bursty code paths. Enqueue send requests in Amazon SQS and consume them with a Lambda function whose event source mapping MaximumConcurrency and batch size keep the aggregate call rate under your account MaxSendRate (read it from the SESv2 GetAccount API). Treat ThrottlingException as retriable by returning the message to the queue, and request a sending quota increase in the SES console when sustained demand is above the quota.
Should I use reserved concurrency or maximum concurrency to rate-limit the SES consumer?
Use the SQS event source mapping maximum concurrency setting. With only reserved concurrency, the event source mapping can poll more messages than the function is allowed to process, which produces throttles and extra receives. Maximum concurrency (2 to 1,000) makes Lambda stop polling at the cap. You can use both; set maximum concurrency below reserved concurrency.
Standard or FIFO queue for SES sending?
Standard queues for most email: throughput is effectively unlimited and duplicates are rare and harmless if your consumer is idempotent (dedupe on a message key). Choose FIFO when you need per-tenant ordering or fairness. Set MessageGroupId to the tenant so one tenant's backlog does not block others, and enable high-throughput mode if you need more than the default 300 transactions per second per API action.
How do I keep one tenant from hurting deliverability for every other tenant?
Send each tenant through its own configuration set or SES tenant, publish bounce and complaint events per configuration set, and alarm on per-tenant rates. When a tenant spikes, pause only that configuration set (SNS to a Lambda function that calls PutConfigurationSetSendingOptions with SendingEnabled=false) while the rest of the account keeps sending. Account-level bounce rate of 5% or more puts the whole account under review, so per-tenant isolation is what keeps one bad list from becoming everyone's problem.
Want this pattern deployed end-to-end?
Our team builds these patterns in production for SaaS, healthcare, fintech, and enterprise customers. Tell us your constraints and we'll scope the engagement.
