Skip to content

554 Recipient count exceeds 50 (Amazon SES)

The error, as SES returns it at the end of DATA:

554 Transaction failed: Recipient count exceeds 50

If you relay through Mailway, this is already handled

Section titled “If you relay through Mailway, this is already handled”

Above a provider’s recipient cap, Mailway rewrites the message so SES accepts it and delivers to everyone. You don’t need to split the send yourself. Skip to what changes for your recipients, because one thing does.

If you send through the HTTP API instead, you never see this error: the API rejects more than 50 recipients up front with a 422, before anything is queued.

This is the part that costs people an afternoon. SES does not count the SMTP envelope. It counts the union of the envelope and the To/Cc/Bcc header addresses, and it evaluates that count after the DATA terminator — so every RCPT TO can be accepted with 250 Ok and the message can still be rejected wholesale a moment later.

Probed against live SES in eu-central-1:

Envelope (RCPT TO) To: header Result
1 51 addresses 554 exceeds 50
2 undisclosed-recipients:; (0) 250 Ok, delivered
51 undisclosed-recipients:; (0) 554 exceeds 50

Row 1 proves the header alone trips the cap — the envelope held one address. Row 3 proves the envelope still counts too — the header was empty. Both are counted, together.

The consequence is the trap: splitting the envelope alone fixes nothing. Send 52 recipients as two chunks of 26 RCPT TO while DATA still carries all 52 addresses in To:, and both chunks fail exactly as the unsplit message did.

This also contradicts the intuition most people bring from AWS’s own “send to multiple recipients” material, which describes the v2 API and its explicit Destinations argument, where header and envelope are decoupled. On the SMTP and SendRawEmail path SES re-derives the count from the message itself. AWS’s troubleshooting documentation is the one that matches observed behaviour: a recipient is any address on the To:, CC:, or BCC: lines.

Above the cap, and only above it, Mailway:

  1. Removes every To/Cc/Bcc header from the raw message, including folded continuation lines, and appends one empty group: To: undisclosed-recipients:;. Header count becomes 0.
  2. Chunks the SMTP envelope into groups of at most the provider’s cap and dials once per chunk.

Union per chunk is then 0 + (≤ cap), which SES accepts. This is classic BCC fan-out.

Two details worth knowing:

  • The body is untouched. The rewrite operates on the header block only and preserves the original line endings, so the body — and therefore the DKIM body hash — is byte-identical. A sender signature that covered To/Cc/Bcc is invalidated, which is acceptable because SES re-signs the modified headers with your domain’s own DKIM and DMARC aligns on that signature.
  • It is the only message mutation in the pipeline. Mailway otherwise relays byte-for-byte. This one exception exists because SES cannot accept the message as sent, and it fires only when the recipient count exceeds the cap. At or below the cap, nothing is rewritten and the envelope is never even computed.

The cap is a per-provider property, not an SES special case. Both SES transports declare 50; providers that impose no such limit take the ordinary single-dial path untouched.

Because the send becomes a BCC:

  • Recipients no longer see each other. Each one sees only the sender and undisclosed-recipients:;.
  • Reply-all reaches the sender only, not the group.

That is the visible cost of SES accepting the message at all, and it’s the industry-standard handling of large recipient sets. If mutual visibility matters for a particular send, keep it under the cap.

Your own records are unaffected. The console mail list, the message detail, and share links read the stored recipient set from the database, not the transmitted headers — so you still see everyone who received it, even though the wire headers say undisclosed-recipients:;.

The first fix Mailway shipped for this was wrong, and the way it failed is why the table above exists.

A company-wide announcement — roughly 52 addresses, all in To: — was rejected wholesale, retried five times, and dead-lettered. The obvious reading was ordinary envelope fan-out, so the first patch split the envelope into chunks of 50 and kept the message byte-identical across them.

It deployed, and the same 554 kept firing. The transcript held the proof: chunk one sent exactly 50 RCPT TO, every one answered 250 Ok, and SES rejected only after DATA. If SES counted the envelope, 50 ≤ 50 would have passed. The failure pointed straight at the headers, and the three probes above confirmed it.