Mailgun vs AWS SES
Mailgun and Amazon SES both court high-volume senders, but with different bargains. SES offers the lowest unit price in the industry and rock-solid infrastructure, in exchange for doing your own deliverability work: warming IPs, processing bounces via SNS, and living without much of a UI. Mailgun costs more per message but includes the operational layer — send-time optimization, inbound routing and parsing, EU region hosting, searchable logs, and human support tiers. The unified message shape exposes a real functional gap too: Mailgun supports per-message metadata and native scheduled delivery while SES supports neither, so a Mailgun-to-SES fallback drops both fields and the SDK will fail fast rather than lose them silently. Teams often start on Mailgun and graduate to SES when volume justifies the operational investment.
Message field support
Field support as encoded in Email SDK's own adapter capability matrix — the same data the SDK uses to reject sends that would silently drop fields.
| Message field | Mailgun | AWS SES |
|---|---|---|
| CC recipients | ✓ | ✓ |
| BCC recipients | ✓ | ✓ |
| Reply-To address | ✓ | ✓ |
| Custom headers | ✓ | ✓ |
| Attachments | ✓ | ✓ |
| Tags | ✓ | ✓ |
| Metadata | ✓ | — |
| Scheduled sending (sendAt) | ✓ | — |
Fallback compatibility
Failing over from Mailgun to AWS SES loses: metadata, scheduled sending (sendat). Email SDK rejects the fallback send for messages that use these fields instead of dropping them silently.
Same code, either provider
With Email SDK the send call is identical for both providers — only the adapter import changes:
import { createEmailClient } from "@opencoredev/email-sdk";
import { mailgun } from "@opencoredev/email-sdk/mailgun";
const client = createEmailClient({
adapters: [mailgun({ apiKey: process.env.MAILGUN_API_KEY!, domain: "mg.yourdomain.com" })],
});
await client.send({
from: "hello@yourdomain.com",
to: "user@example.com",
subject: "Welcome!",
html: "<p>It works.</p>",
});Or run both, with automatic fallback:
import { createEmailClient } from "@opencoredev/email-sdk";
import { mailgun } from "@opencoredev/email-sdk/mailgun";
import { ses } from "@opencoredev/email-sdk/ses";
const client = createEmailClient({
adapters: [
mailgun({ apiKey: process.env.MAILGUN_API_KEY!, domain: "mg.yourdomain.com" }),
ses({
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: "us-east-1",
}),
],
defaultAdapter: "mailgun",
fallback: ["ses"],
});FAQ
- Can I switch from Mailgun to AWS SES without rewriting my email code?
- Yes. With Email SDK both providers share one typed send() call and one message shape, so switching from Mailgun to AWS SES is a one-line adapter change plus an API key. The SDK fails fast if a message uses a field AWS SES does not support, so nothing is silently dropped.
- Which message fields does Mailgun support that AWS SES doesn't?
- Mailgun supports metadata, scheduled sending (sendat) in the unified message shape, which AWS SES does not.
- Can I use AWS SES as a fallback for Mailgun?
- Partially. Email SDK checks field support before every send: a fallback from Mailgun to AWS SES is rejected for messages using metadata, scheduled sending (sendat). Messages that avoid those fields fail over cleanly.
Adapter docs
Setup guides: Mailgun adapter and AWS SES adapter. Sending from your own domain? Verify SPF, DKIM, and DMARC with the free email DNS checker.