Email Rate Limiting: Queue Work Without Overloading Your Provider
Control email dispatch across workers, classify throttling responses, preserve idempotency and monitor queue age for time-sensitive messages.
TL;DR
- Put outbound email in a durable queue and control dispatch against the provider's current capacity.
- Separate retryable throttling from authentication, validation and policy failures that need a different response.
- Preserve notification identity and payload across retries, and respect Retry-After when the provider supplies it.
- Monitor queue age as well as send volume so time-sensitive messages do not become useless while waiting.
A queue is only useful when it controls pressure
Moving email into a background worker prevents a web request from waiting on delivery, but it does not automatically protect the provider. A hundred workers draining the same backlog can create a larger burst than the original application traffic.
Define a shared dispatch budget for the account or sending scope that is actually limited. A separate in-memory limiter in each worker does not enforce a shared ceiling. Include all applications that use the same provider account when calculating pressure.
Keep the durable queue as the record of intended work. A worker claims a notification, checks its eligibility and attempts submission under the shared budget. It records the result before deciding whether another attempt is appropriate.
Distinguish rate limits from other sending limits
Providers may enforce requests per interval, recipients per interval, daily quotas, message size or reputation-related capacity. These are different constraints. Sending smaller batches may change request pressure without reducing the total recipient count.
Amazon SES, for example, distinguishes daily sending quota from sending rate and notes that actual acceptance can be below the maximum configured rate. Those account-specific limits should not be replaced with a hard-coded number copied from a tutorial. See managing SES sending quotas.
For MailBlastr, inspect the current response and API error reference. Some capacity conditions are retryable, while policy pauses and invalid requests require intervention. A generic “retry every failure” rule can waste capacity and hide the problem.
Classify the result before scheduling another attempt
For a documented retryable response, respect Retry-After when present and preserve the same notification identity. If no explicit delay is supplied and the operation is safe to retry, use bounded backoff with jitter so many workers do not restart simultaneously.
Authentication failures need a credential or permission fix. Invalid recipients or oversized payloads need validation or data repair. A policy pause should trigger the documented review process. Increasing concurrency cannot solve any of those conditions.
A network timeout is especially important: the provider may have accepted the message even though the application never received the response. Use the provider's idempotency behavior and your stored payload to reconcile or safely retry. Do not treat uncertainty as permission to create a fresh send.
Make queue ownership explicit
Each claimed job needs an ownership mechanism that prevents another worker from submitting it concurrently. Use a transactional claim or lease suited to your queue, and define what happens if the worker dies while holding it.
Store attempt count, next eligible time, last error category and provider submission identity. Keep message content and credentials out of broad operational logs. A support view can show the relevant state without revealing every private payload.
When a lease expires after an uncertain submission, reconciliation matters more than speed. Retrying with a new key may produce duplicates. The email idempotency guide covers the durable identity that lets the application reason about that boundary.
Give time-sensitive work a useful lifetime
Password resets, verification links and invitations may become stale. Define when each notification stops being useful, and check that expiry before dispatch. A queue should not celebrate draining a backlog by sending yesterday's expired recovery links.
Use priorities or separate queues when different message categories have different latency needs. Reserve capacity appropriately rather than letting a large newsletter occupy every dispatch slot while account emails wait. Avoid starvation by giving lower-priority work a deliberate policy too.
Record intentionally expired or superseded jobs separately from provider failures. That makes reporting honest: the application decided not to send obsolete work, rather than the provider losing it.
Monitor the backlog, not just throughput
Useful signals include oldest eligible job age, retry counts by error category, successful submissions, delivery outcomes and the number of jobs that expire before dispatch. Track them by message category where the distinction changes the response.
An increasing backlog with a steady send rate may be normal during a planned import. The same pattern in password resets may require immediate action. Set alerts around the service expectation for each flow, not a universal queue-size threshold.
Use a controlled load test to confirm the limiter works across multiple workers. Simulate throttling, a timeout after acceptance, a worker crash and a daily-capacity exhaustion. Verify that the system pauses or reschedules without duplicating messages or spinning in a tight loop.
Keep the operational response simple
When capacity is constrained, reduce intake or dispatch according to the queue policy and inspect the documented cause. If a larger legitimate workload requires more capacity, request it through the provider's supported process.
MailBlastr can return the sending result, but your application owns the backlog and its priorities. Pair capacity handling with the migration checklist whenever you change providers, because different limits and retry semantics can expose assumptions that were invisible at lower volume.