Email Attachment Size Limits and Base64 Overhead
Most email size limits count the message as it travels, not the files you attached. Attachments are base64-encoded on the way, which makes them about 37% bigger.
The Encoding Overhead
SMTP carries text, so a binary attachment is base64-encoded: every 3 bytes become 4 characters, a third more. MIME then wraps the encoded text at 76 characters per line, and every line break adds 2 bytes:
encoded size ≈ file size × 4/3 × 78/76 ≈ file size × 1.37
The largest attachment that fits is about 73% of the limit, a little less once you count headers and the body:
| Message limit | Largest attachment, roughly |
|---|---|
| 10 MB | 7.3 MB |
| 20 MB | 14.6 MB |
| 25 MB | 18.3 MB |
| 30 MB | 21.9 MB |
| 35 MB | 25.6 MB |
| 40 MB | 29.2 MB |
Attachments share one limit. Two 10 MB files make a message of about 27 MB.
Published Limits
| Service | Limit | Counts |
|---|---|---|
| Gmail, personal accounts | 25 MB | Attachments on a sent message. Over the limit, Gmail swaps them for a Google Drive link |
| Yahoo Mail | 25 MB | All attachments in one message, sent or received |
| Microsoft 365, Exchange Online | 35 MB to send, 36 MB to receive | The whole message, by default. Admins can set anything from 1 to 150 MB |
| Amazon SES | 40 MB | The whole message after base64 encoding, over SMTP or the v2 API. The v1 API allows 10 MB |
| SendGrid | Under 30 MB | The whole message, through the v3 Mail Send API |
| Mailgun | 25 MB | The whole message |
| Postmark | 10 MB | The whole message. The text and HTML bodies can be up to 5 MB each |
A message passes at least two of these limits: your provider's on the way out, and the recipient's on the way in. The smaller one wins. A 38 MB message that SES accepts still bounces at a Microsoft 365 mailbox on the default 36 MB receive limit.
Every SMTP server also states its own limit. Connect, send EHLO, and the SIZE line in the reply is the maximum in bytes (how). send.smtp.dev replies SIZE 20971520, which is 20 MB.
Testing the Boundary
Size bugs show up in two places: the message that's too big, and what your app does when a server refuses it. Both are easy to reproduce in the sandbox.
Make one file that fits and one that doesn't:
head -c 12000000 /dev/urandom > fits.bin
head -c 18000000 /dev/urandom > too-big.bin
Encoded, fits.bin is about 16 MB and goes through send.smtp.dev. too-big.bin is about 25 MB, so the server refuses it with 552:
swaks --server send.smtp.dev --port 587 --tls \
--auth-user sender@test.example.com --auth-password "$SMTP_PASSWORD" \
--from sender@test.example.com --to qa@test.example.com \
--attach @too-big.bin
For the message that arrived, the API reports the size of the message and of each attachment. Compare them with what your app attached:
curl "https://api.smtp.dev/accounts/{accountId}/mailboxes/{mailboxId}/messages/{id}" \
-H "X-API-KEY: $SMTP_DEV_API_KEY" | jq '{size, attachments: [.attachments[] | {filename, size}]}'
Then send the oversized message through your app and check what happens next:
- Check before sending. Estimate the encoded size from the file sizes, and send a download link instead of an attachment past your provider's limit. That's what Gmail does with Drive.
- Treat
552as permanent. Retrying a message that's too big fails the same way every time. - Tell someone. The person who triggered the email should learn it didn't go out.
Local Development
Mailpit, MailHog, MailCatcher, and smtp4dev catch email on your machine. What each one does, where localhost stops working, and when a hosted sandbox fits.
Mailtrap
Mailtrap's Email Sandbox and smtp.dev side by side on quotas, retention, and inbound addresses. When to stay on Mailtrap, and how to move a test suite.