Recipient rate guards

Caps that stop the same mailbox from being hammered — 5 per 10 minutes, 20 per 24 hours, and bounce cooldowns.

Lumail blocks a single recipient address from receiving too many real emails, even if the sender is allowed to send. Campaigns, workflows, transactional API, and SMTP share this gate. It is not your plan quota and not the HTTP API rate limit.

The address is hashed (SHA-256 of the normalized envelope) before it is stored in Redis. Fixture / test-mode recipients skip the gate entirely — see Test-mode recipients.

Caps

RuleLimitWindow
Frequency5 accepted sendsrolling 10 minutes
Frequency20 accepted sendsrolling 24 hours
Bounce cooldownno further sends24 hours after a recipient-rate or mailbox-full bounce

A send counts only when the SES-boundary grant succeeds. Peeking the windows (API preflight) does not increment them.

The 24-hour frequency cap is the tighter one for a blast to one inbox: at most 20 real emails per address per day.

What trips the 24-hour cooldown

A later bounce in these categories writes a per-recipient cooldown key. Until it expires, every send to that address is refused:

  • Gmail / ISP recipient rate throttled
  • Mailbox full

Coarse ISP labels (for example Other) never pause unrelated recipients. Guards are recipient-only.

Who it applies to

PathGuarded?When it refuses
Transactional API (POST /api/v1/emails, POST /api/v2/emails, batch)YesBefore queue if the window is already full. At send if a burst queued first.
SMTP (smtp.lumail.io)YesSame transactional lane and the same Redis windows.
Campaigns and workflow emailsYesAt send. Already-enrolled recipients that hit the cap are skipped, not retried.
Test-mode / reserved fixture domainsNoFake SES id, no window increment.

Transactional traffic is TIME_SENSITIVE: it jumps ahead of campaign bulk for the shared SES limiter. The recipient caps still apply. A 20k burst to one Gmail does not starve the guard, but while those rows drain or skip they can delay bulk campaigns.

What you see

Transactional API — refused at enqueue

The address already sits at 5/10 min or 20/24 h (or is on cooldown). Lumail does not create the email or the journal row.

{
  "message": "This recipient is temporarily rate limited",
  "code": "RECIPIENT_RATE_LIMITED",
  "retryAfter": 86400
}

HTTP 429, plus Retry-After in seconds. Honor that header. Do not treat this as the organization API rpm limit — that 429 uses a different body (Too many requests) and is documented under Rate Limits.

Transactional / campaign — refused at send

A burst can pass preflight while the windows are still empty, then lose at the SES fence. Those rows become journal phase SKIPPED with RECIPIENT_RATE_LIMITED. No provider send. Campaigns do not retry them. Transactional placeholders stay unsent (awsEmailId null).

One address, 20 000 transactional emails in two hours

  1. The first 20 grants increment the 24-hour window and go to SES (subject to org quota and API rpm).
  2. Call 21 onward: 429 RECIPIENT_RATE_LIMITED if you send after those 20 have been granted.
  3. If you enqueue all 20 000 before any grant lands, ~20 send and the rest SKIPPED.
  4. Gmail sees at most 20 messages. The rest never leave Lumail.
  5. The 429 / skip lasts until the oldest grant ages out of the 24-hour window (up to 24 h).

Without an Idempotency-Key, each call is a distinct dispatch. You still get one subscriber row.

Not this guard

SymptomWhat it actually is
429 Too many requestsOrganization HTTP rpm. Rate Limits.
Monthly / daily plan capLumail billing quota. How to fix a monthly email limit.
Sending rate exceeded / daily SES quotaShared provider account cap. How to fix sending rate or daily quota.
Slow campaign (for example ~10 emails/s)Provider RTT and retries after the grant. Recipient guards are not a global emails/s cap.