Mailgun to AWS SES Migration
Migrating from Mailgun to AWS SES: Step-by-Step Guide
Step-by-step migration guide for engineers moving off Mailgun — pricing breakdown, routing rule equivalents, GDPR-compliant EU region setup, and SNS-based webhook event handling.
Mailgun has long been the go-to for developers who want a clean REST API and minimal operational overhead. AWS SES is where teams land when they prioritize cost efficiency and AWS ecosystem consolidation over out-of-the-box simplicity. If your stack is already on AWS, the migration pays for itself quickly.
## Related Comparisons
Explore other technical comparisons:
- [SendGrid to AWS SES](/compare/sendgrid-to-aws-ses/)
- [Postmark to AWS SES](/compare/postmark-to-aws-ses/)
## Why Teams Migrate from Mailgun to SES
Cost is the immediate trigger for most migrations. Mailgun's pricing model has shifted over the years — the legacy free tier is gone, and the current Foundation plan carries a meaningful monthly fixed cost.
| Volume | Mailgun Foundation | Mailgun Flex | AWS SES | SES Savings vs Foundation |
| -------------- | -------------------- | ------------- | ------------ | ------------------------- |
| 10,000 emails | $35/month (included) | $8.00/month | $1.00/month | $34.00 |
| 50,000 emails | $35/month (included) | $40.00/month | $5.00/month | $30.00 |
| 100,000 emails | $80/month (overage) | $80.00/month | $10.00/month | $70.00 |
| 500,000 emails | ~$360/month | $400.00/month | $50.00/month | $310.00 |
Mailgun Flex charges $0.80 per 1,000 emails with no monthly minimum — 8x the SES rate. Even the Foundation plan's included 50,000 emails work out to $0.70 per 1,000 against SES's $0.10.
## API Migration: Mailgun → AWS SES
Mailgun's Messages API and SES share the same conceptual model but use different request formats.
| Mailgun API | AWS SES Equivalent | Notes |
| --------------------------------------- | ----------------------------------------------- | --------------------------------- |
| `POST /v3/{domain}/messages` | `SendEmail` / `SendRawEmail` | Core send operation |
| `from`, `to`, `subject`, `text`, `html` | `Source`, `Destination`, `Message` | Direct field mapping |
| API Key (Basic auth) | IAM access key or SES SMTP credentials | Use IAM for AWS-native apps |
| SMTP (smtp.mailgun.org, port 587) | `email-smtp.[region].amazonaws.com`, port 587 | Drop-in SMTP replacement |
| `h:X-Mailgun-*` custom headers | `MessageAttributes` or raw message headers | Same capability, different syntax |
| `o:tag` message tags | Configuration Set tags + SNS message attributes | Equivalent for event filtering |
If your application uses SMTP, the migration is a credentials swap. Update the SMTP host to `email-smtp.us-east-1.amazonaws.com` (or your chosen region), generate SES SMTP credentials from the SES console, and update your application config. No code changes required.
## Routing Rules: Mailgun Routes → SES Receipt Rules
Mailgun Routes allow you to match inbound email by recipient pattern or domain and forward, store, or call a webhook. SES Receipt Rules provide the same capabilities with deeper AWS integration.
**Mailgun route (example):**
```
match_recipient(".*@support.example.com")
→ forward("https://app.example.com/inbound")
→ store(notify="https://app.example.com/stored")
```
**SES Receipt Rule equivalent:**
1. Create a Rule Set in SES Receipt Rules
2. Add a rule matching `@support.example.com`
3. Set actions: Publish to SNS → Lambda function that calls your webhook endpoint
The SNS → Lambda pattern is more verbose to configure but gives you full programmatic control over inbound email routing, parsing, and processing. For teams comfortable with Lambda, it is the more powerful option.
## EU Data Residency: Mailgun EU → SES eu-west-1
Mailgun offers a dedicated EU region (api.eu.mailgun.net) to keep email data within Europe for GDPR compliance. SES supports the same via region selection.
**SES regions for EU data residency:**
- `eu-west-1` (Ireland) — primary EU region, lowest latency to EU users
- `eu-central-1` (Frankfurt) — German data residency if required
- `eu-west-2` (London) — UK data residency post-Brexit
Configure your SES SDK client to use the appropriate regional endpoint. DNS verification records (DKIM CNAME, MAIL FROM MX) must be added for each region separately — you cannot share verification between SES regions.
## Webhook Event Format Mapping
Mailgun fires webhook POSTs to your endpoint for each email event. SES routes events through SNS, which then calls your HTTP endpoint (via SNS HTTP subscription) or Lambda.
| Mailgun Event | SES/SNS Event Type | Key Fields |
| -------------------- | -------------------- | --------------------------------------------------------------------------- |
| `delivered` | `Delivery` | `timestamp`, `recipients[]`, `smtpResponse`, `reportingMTA` |
| `failed` (permanent) | `Bounce` (Hard) | `bounceType: "Permanent"`, `bouncedRecipients[].emailAddress`, `timestamp` |
| `failed` (temporary) | `Bounce` (Soft) | `bounceType: "Transient"`, `bouncedRecipients[].emailAddress`, `timestamp` |
| `complained` | `Complaint` | `complainedRecipients[].emailAddress`, `complaintFeedbackType`, `timestamp` |
| `opened` | `Open` | `timestamp`, `ipAddress`, `userAgent`, `destination` |
| `clicked` | `Click` | `timestamp`, `ipAddress`, `userAgent`, `link` |
| `unsubscribed` | No native equivalent | Must implement via List-Unsubscribe header + custom application handler |
| `stored` | No equivalent | SES does not store messages; use S3 action via Receipt Rules |
**Important:** SES events arrive wrapped in an SNS notification envelope. The `Message` field of the SNS notification contains a JSON string (double-encoded) of the actual SES event. Your Lambda handler must `JSON.parse(event.Records[0].Sns.Message)` to access the SES event object.
## Email Authentication on SES: SPF, DKIM, DMARC, BIMI
Mailgun handles authentication via your verified sending domain — the dashboard publishes the SPF, DKIM, and tracking CNAMEs you need, and Mailgun signs outbound mail with their key infrastructure. SES makes you own the same surface with no managed defaults. This is good for control, and unavoidable in 2026 — Gmail, Yahoo, and Microsoft now treat unauthenticated mail from any sender above 5,000 messages per day as effectively undeliverable.
**SPF.** Add `include:amazonses.com` to your sending domain's SPF record. If you are keeping Mailgun running in parallel during a phased cutover, your record looks like `v=spf1 include:mailgun.org include:amazonses.com -all` until you fully cut over. Watch the 10-DNS-lookup limit — chaining too many `include:` directives silently breaks SPF and is the most common deliverability regression during a Mailgun-to-SES migration.
**DKIM.** Enable Easy DKIM in the SES console for each verified identity and publish the three CNAME records SES generates. Easy DKIM rotates keys automatically. During the cutover window where both Mailgun and SES are sending, both DKIM keys validate independently because they use different selectors — Mailgun's `krs._domainkey.[your-domain]` (or `pic._domainkey.[your-domain]` for newer accounts) and SES's three CNAMEs coexist without conflict.
**DMARC.** Publish `_dmarc.yourdomain.com` with at minimum `v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com` on day one to start collecting aggregate reports. Move to `p=quarantine` after two to four weeks of clean reports, then to `p=reject` once every legitimate sender — transactional, marketing, internal tooling, calendar invites — is aligned. As of February 2024, Gmail and Yahoo require DMARC for any sender exceeding 5,000 messages per day; `p=none` meets the minimum, but enforcement is what actually blocks spoofing.
**BIMI.** Once you reach `p=quarantine` or stricter and have a Verified Mark Certificate (VMC), publish a BIMI record so your logo renders next to messages in Gmail, Apple Mail, and Yahoo. BIMI does not directly improve deliverability, but the trust signal lifts open rates measurably for transactional and lifecycle email.
**Common Mailgun DNS cleanup mistakes after cutover:**
- Leaving Mailgun MX records (`mxa.mailgun.org`, `mxb.mailgun.org`) on a domain you also use for SES inbound — the older MX entries keep stealing inbound mail and breaking SES Receipt Rules. Remove them only after you have verified SES inbound works end-to-end.
- Leaving the Mailgun tracking CNAME (`email.yourdomain.com` pointing to `mailgun.org`) live after you have stopped sending from Mailgun — orphaned tracking domains cause hard-to-diagnose dead links in archived emails and pollute DMARC alignment reports.
- Forgetting to add SPF and DKIM for the EU SES region if you have split sending across regions — verification, DKIM, and SPF are per-region in SES, unlike Mailgun's global EU/US split.
## Reputation, IP Warming, and List Hygiene After Mailgun
Mailgun customers on Foundation or Flex share a warmed IP pool. The reputation built on those IPs belongs to Mailgun and does not transfer to SES — you start cold the day you cut over, regardless of how clean your domain history is. Plan for it.
**Migrating to SES shared IPs (most senders).** The SES shared pool is suitable for transactional senders under 500,000 emails per month. There is no formal warming required, but mailbox providers still observe sending velocity per domain. Ramp gradually over 7–10 days rather than spiking from zero to full volume on day one.
**Migrating to SES dedicated IPs (high-volume marketing).** SES dedicated IPs cost $24.95 per IP per month — meaningfully cheaper than Mailgun's $59/month dedicated IPs. The trade-off is that you handle warming yourself.
| Day Range | Daily Volume Cap | Notes |
| ---------- | ------------------ | ---------------------------------------- |
| Days 1–3 | 200 emails/day | Send to most engaged segment only |
| Days 4–7 | 1,000 emails/day | Watch bounce rate (target <2%) |
| Days 8–14 | 10,000 emails/day | Add second engagement tier |
| Days 15–21 | 50,000 emails/day | Monitor complaint rate (target <0.1%) |
| Days 22–30 | Full target volume | Reputation established |
Keep Mailgun running in parallel for non-engaged segments during the warming window. The dual-vendor month is the cost of insurance against a deliverability dip that can take 60+ days to recover from.
**List hygiene before cutover.** Reputation built on a clean list is durable; reputation built on a stale list is borrowed time.
- Export Mailgun's bounce, unsubscribe, and complaint history via the Events API and import the hard bounces and complaints to the SES account-level suppression list before the first production send. The 7-day event log retention on Mailgun Foundation means anything older than a week is already lost — pull what you can now.
- Suppress addresses that have not engaged in the last 6–12 months for marketing streams. Re-engagement campaigns belong on the legacy provider, not on a fresh SES IP.
- Run a syntax + MX validation pass on your active list. Typo'd domains (`gmial.com`, `hotnail.com`), role accounts, and disposable mailboxes inflate the bounce rate and burn reputation while warming.
**From Mailgun tags to SES Configuration Sets.** Mailgun's `o:tag` lets you classify sends for analytics. The SES equivalent is a Configuration Set per sending stream — transactional, marketing, lifecycle, internal — with separate event destinations and, optionally, separate dedicated IP pools. Isolating streams means a marketing reputation hit does not take down password resets.
**Sending cadence and throttling.** SES enforces a per-second send rate and a 24-hour quota that grow with reputation. Smooth bulk sends across hours rather than firing at minute zero — bursty patterns trigger greylisting at smaller mailbox providers and look like list-bombing to spam classifiers. Use `SendBulkEmail` with batched destinations and throttle at the SDK or queue layer.
**Engagement-based filtering.** The single highest-leverage change most teams make post-migration is to stop sending to disengaged subscribers entirely. Maintain a "send eligibility" flag in your subscriber store, updated nightly from SES open and click events, and exclude anyone who has not engaged in your defined window. Gmail and Microsoft weight recent positive engagement (opens, replies, "move to inbox") far more heavily than total volume, so a smaller, hotter list lifts inbox placement for the entire domain — typically from the mid-80s to the mid-90s within 30 days.
## Production Event Tracking: SES → Kinesis Firehose → S3 → Node.js
Mailgun gives you a polished event log with 7 days of retention on Foundation and 30 days on Flex/Growth. After cutover to SES, the SNS → Lambda pattern from the webhook section above handles tens of thousands of events per day. Past that volume, or when you need historical analytics that outlive Mailgun's hosted window, the production-grade pipeline is **SES → Kinesis Data Firehose → S3 → Node.js API**.
**How it works.**
1. **SES Configuration Set** — define one per sending stream (transactional, marketing, lifecycle) and attach a Kinesis Firehose event destination. Subscribe to all event types: `send`, `delivery`, `bounce`, `complaint`, `open`, `click`, `reject`, `renderingFailure`, `deliveryDelay`, and `subscription`.
2. **Kinesis Data Firehose** — buffers events (typically 60 seconds or 5 MB) and writes newline-delimited JSON to S3 partitioned by `year/month/day/hour`. Enable dynamic partitioning to split by Configuration Set or sending IP for downstream filtering.
3. **S3 with lifecycle policy** — Standard for 30 days (hot analytics window), Standard-IA at 30 days, Glacier Flexible Retrieval at 180 days. Raw event archives are the cheapest part of the stack and the most useful during deliverability investigations.
4. **Node.js API** — consume events by tailing new S3 objects via S3 Event Notifications → SQS → Node.js worker (Express or Fastify behind an ALB, or a Lambda function for spiky traffic), or query historical data via Amazon Athena. The API exposes per-recipient timelines, per-campaign engagement, and per-domain placement metrics to your dashboards.
**Why this beats Mailgun's hosted event log.**
- **Retention.** S3 with Glacier transition gives you years of event history for a few dollars a month. Mailgun's hosted log capped retroactive analytics at 7–30 days; you can do better.
- **Replayable.** S3 is the source of truth. Rebuild a recipient timeline, re-run a deliverability analysis, or backfill a new dashboard without re-fetching from a vendor API.
- **Cheap analytics.** Athena over partitioned S3 answers "what was my Gmail open rate by sending IP last week?" in seconds, not engineering tickets.
- **Audit trail.** Compliance and abuse investigations require raw event history. Firehose-to-S3 gives it to you immutably.
**Filtering bots, proxies, and Apple MPP.** SES open events fire on pixel load and click events fire on the SES tracking redirector. Both are noisy: corporate proxies, link-scanning gateways, and Apple Mail Privacy Protection pre-fetch images and follow links, generating engagement signals that have nothing to do with a human reading the message. Without filtering, your "open rate" double-counts machine activity and your engagement-based suppression logic ends up suppressing real subscribers.
[InboxEagle's Bot Finder](https://www.inboxeagle.com/) sits naturally between the S3 raw layer and the engagement-based filtering layer described above — it analyzes SES open and click streams, separates human engagement from bot and proxy activity (Apple MPP, Microsoft Defender, Gmail image proxy, link-scanning gateways), and feeds your Node.js API cleaned engagement data so your reputation signals reflect real subscriber behavior. Teams migrating off Mailgun's pre-aggregated event UI underestimate how much inbox-placement signal lives in distinguishing real opens from scanner activity; a bot filter is the difference between an engagement model that improves placement and one that quietly degrades it.
## Why Choose FactualMinds for Your Email Migration
FactualMinds is an **AWS Select Tier Consulting Partner** specializing in email infrastructure migration. We have executed SendGrid, Mailgun, Postmark, and SparkPost to AWS SES migrations and know exactly where teams get stuck.
- **Email migration experts** — we handle domain verification, DKIM, bounce architecture, IP warming
- **Assessment-first approach** — we map your current state before writing a line of infrastructure code
- **Zero-downtime cutover planning included** — no failed deliveries during migration
- **AWS Select Tier Partner** — [verified on AWS Partner Network](https://partners.amazonaws.com/partners/001aq000008su2EAAQ/Factual%20Minds)
---Mailgun has long been the go-to for developers who want a clean REST API and minimal operational overhead. AWS SES is where teams land when they prioritize cost efficiency and AWS ecosystem consolidation over out-of-the-box simplicity. If your stack is already on AWS, the migration pays for itself quickly.
Related Comparisons
Explore other technical comparisons:
Why Teams Migrate from Mailgun to SES
Cost is the immediate trigger for most migrations. Mailgun’s pricing model has shifted over the years — the legacy free tier is gone, and the current Foundation plan carries a meaningful monthly fixed cost.
| Volume | Mailgun Foundation | Mailgun Flex | AWS SES | SES Savings vs Foundation |
|---|---|---|---|---|
| 10,000 emails | $35/month (included) | $8.00/month | $1.00/month | $34.00 |
| 50,000 emails | $35/month (included) | $40.00/month | $5.00/month | $30.00 |
| 100,000 emails | $80/month (overage) | $80.00/month | $10.00/month | $70.00 |
| 500,000 emails | ~$360/month | $400.00/month | $50.00/month | $310.00 |
Mailgun Flex charges $0.80 per 1,000 emails with no monthly minimum — 8x the SES rate. Even the Foundation plan’s included 50,000 emails work out to $0.70 per 1,000 against SES’s $0.10.
API Migration: Mailgun → AWS SES
Mailgun’s Messages API and SES share the same conceptual model but use different request formats.
| Mailgun API | AWS SES Equivalent | Notes |
|---|---|---|
POST /v3/{domain}/messages | SendEmail / SendRawEmail | Core send operation |
from, to, subject, text, html | Source, Destination, Message | Direct field mapping |
| API Key (Basic auth) | IAM access key or SES SMTP credentials | Use IAM for AWS-native apps |
| SMTP (smtp.mailgun.org, port 587) | email-smtp.[region].amazonaws.com, port 587 | Drop-in SMTP replacement |
h:X-Mailgun-* custom headers | MessageAttributes or raw message headers | Same capability, different syntax |
o:tag message tags | Configuration Set tags + SNS message attributes | Equivalent for event filtering |
If your application uses SMTP, the migration is a credentials swap. Update the SMTP host to email-smtp.us-east-1.amazonaws.com (or your chosen region), generate SES SMTP credentials from the SES console, and update your application config. No code changes required.
Routing Rules: Mailgun Routes → SES Receipt Rules
Mailgun Routes allow you to match inbound email by recipient pattern or domain and forward, store, or call a webhook. SES Receipt Rules provide the same capabilities with deeper AWS integration.
Mailgun route (example):
match_recipient(".*@support.example.com")
→ forward("https://app.example.com/inbound")
→ store(notify="https://app.example.com/stored")SES Receipt Rule equivalent:
- Create a Rule Set in SES Receipt Rules
- Add a rule matching
@support.example.com - Set actions: Publish to SNS → Lambda function that calls your webhook endpoint
The SNS → Lambda pattern is more verbose to configure but gives you full programmatic control over inbound email routing, parsing, and processing. For teams comfortable with Lambda, it is the more powerful option.
EU Data Residency: Mailgun EU → SES eu-west-1
Mailgun offers a dedicated EU region (api.eu.mailgun.net) to keep email data within Europe for GDPR compliance. SES supports the same via region selection.
SES regions for EU data residency:
eu-west-1(Ireland) — primary EU region, lowest latency to EU userseu-central-1(Frankfurt) — German data residency if requiredeu-west-2(London) — UK data residency post-Brexit
Configure your SES SDK client to use the appropriate regional endpoint. DNS verification records (DKIM CNAME, MAIL FROM MX) must be added for each region separately — you cannot share verification between SES regions.
Webhook Event Format Mapping
Mailgun fires webhook POSTs to your endpoint for each email event. SES routes events through SNS, which then calls your HTTP endpoint (via SNS HTTP subscription) or Lambda.
| Mailgun Event | SES/SNS Event Type | Key Fields |
|---|---|---|
delivered | Delivery | timestamp, recipients[], smtpResponse, reportingMTA |
failed (permanent) | Bounce (Hard) | bounceType: "Permanent", bouncedRecipients[].emailAddress, timestamp |
failed (temporary) | Bounce (Soft) | bounceType: "Transient", bouncedRecipients[].emailAddress, timestamp |
complained | Complaint | complainedRecipients[].emailAddress, complaintFeedbackType, timestamp |
opened | Open | timestamp, ipAddress, userAgent, destination |
clicked | Click | timestamp, ipAddress, userAgent, link |
unsubscribed | No native equivalent | Must implement via List-Unsubscribe header + custom application handler |
stored | No equivalent | SES does not store messages; use S3 action via Receipt Rules |
Important: SES events arrive wrapped in an SNS notification envelope. The Message field of the SNS notification contains a JSON string (double-encoded) of the actual SES event. Your Lambda handler must JSON.parse(event.Records[0].Sns.Message) to access the SES event object.
Email Authentication on SES: SPF, DKIM, DMARC, BIMI
Mailgun handles authentication via your verified sending domain — the dashboard publishes the SPF, DKIM, and tracking CNAMEs you need, and Mailgun signs outbound mail with their key infrastructure. SES makes you own the same surface with no managed defaults. This is good for control, and unavoidable in 2026 — Gmail, Yahoo, and Microsoft now treat unauthenticated mail from any sender above 5,000 messages per day as effectively undeliverable.
SPF. Add include:amazonses.com to your sending domain’s SPF record. If you are keeping Mailgun running in parallel during a phased cutover, your record looks like v=spf1 include:mailgun.org include:amazonses.com -all until you fully cut over. Watch the 10-DNS-lookup limit — chaining too many include: directives silently breaks SPF and is the most common deliverability regression during a Mailgun-to-SES migration.
DKIM. Enable Easy DKIM in the SES console for each verified identity and publish the three CNAME records SES generates. Easy DKIM rotates keys automatically. During the cutover window where both Mailgun and SES are sending, both DKIM keys validate independently because they use different selectors — Mailgun’s krs._domainkey.[your-domain] (or pic._domainkey.[your-domain] for newer accounts) and SES’s three CNAMEs coexist without conflict.
DMARC. Publish _dmarc.yourdomain.com with at minimum v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com on day one to start collecting aggregate reports. Move to p=quarantine after two to four weeks of clean reports, then to p=reject once every legitimate sender — transactional, marketing, internal tooling, calendar invites — is aligned. As of February 2024, Gmail and Yahoo require DMARC for any sender exceeding 5,000 messages per day; p=none meets the minimum, but enforcement is what actually blocks spoofing.
BIMI. Once you reach p=quarantine or stricter and have a Verified Mark Certificate (VMC), publish a BIMI record so your logo renders next to messages in Gmail, Apple Mail, and Yahoo. BIMI does not directly improve deliverability, but the trust signal lifts open rates measurably for transactional and lifecycle email.
Common Mailgun DNS cleanup mistakes after cutover:
- Leaving Mailgun MX records (
mxa.mailgun.org,mxb.mailgun.org) on a domain you also use for SES inbound — the older MX entries keep stealing inbound mail and breaking SES Receipt Rules. Remove them only after you have verified SES inbound works end-to-end. - Leaving the Mailgun tracking CNAME (
email.yourdomain.compointing tomailgun.org) live after you have stopped sending from Mailgun — orphaned tracking domains cause hard-to-diagnose dead links in archived emails and pollute DMARC alignment reports. - Forgetting to add SPF and DKIM for the EU SES region if you have split sending across regions — verification, DKIM, and SPF are per-region in SES, unlike Mailgun’s global EU/US split.
Reputation, IP Warming, and List Hygiene After Mailgun
Mailgun customers on Foundation or Flex share a warmed IP pool. The reputation built on those IPs belongs to Mailgun and does not transfer to SES — you start cold the day you cut over, regardless of how clean your domain history is. Plan for it.
Migrating to SES shared IPs (most senders). The SES shared pool is suitable for transactional senders under 500,000 emails per month. There is no formal warming required, but mailbox providers still observe sending velocity per domain. Ramp gradually over 7–10 days rather than spiking from zero to full volume on day one.
Migrating to SES dedicated IPs (high-volume marketing). SES dedicated IPs cost $24.95 per IP per month — meaningfully cheaper than Mailgun’s $59/month dedicated IPs. The trade-off is that you handle warming yourself.
| Day Range | Daily Volume Cap | Notes |
|---|---|---|
| Days 1–3 | 200 emails/day | Send to most engaged segment only |
| Days 4–7 | 1,000 emails/day | Watch bounce rate (target <2%) |
| Days 8–14 | 10,000 emails/day | Add second engagement tier |
| Days 15–21 | 50,000 emails/day | Monitor complaint rate (target <0.1%) |
| Days 22–30 | Full target volume | Reputation established |
Keep Mailgun running in parallel for non-engaged segments during the warming window. The dual-vendor month is the cost of insurance against a deliverability dip that can take 60+ days to recover from.
List hygiene before cutover. Reputation built on a clean list is durable; reputation built on a stale list is borrowed time.
- Export Mailgun’s bounce, unsubscribe, and complaint history via the Events API and import the hard bounces and complaints to the SES account-level suppression list before the first production send. The 7-day event log retention on Mailgun Foundation means anything older than a week is already lost — pull what you can now.
- Suppress addresses that have not engaged in the last 6–12 months for marketing streams. Re-engagement campaigns belong on the legacy provider, not on a fresh SES IP.
- Run a syntax + MX validation pass on your active list. Typo’d domains (
gmial.com,hotnail.com), role accounts, and disposable mailboxes inflate the bounce rate and burn reputation while warming.
From Mailgun tags to SES Configuration Sets. Mailgun’s o:tag lets you classify sends for analytics. The SES equivalent is a Configuration Set per sending stream — transactional, marketing, lifecycle, internal — with separate event destinations and, optionally, separate dedicated IP pools. Isolating streams means a marketing reputation hit does not take down password resets.
Sending cadence and throttling. SES enforces a per-second send rate and a 24-hour quota that grow with reputation. Smooth bulk sends across hours rather than firing at minute zero — bursty patterns trigger greylisting at smaller mailbox providers and look like list-bombing to spam classifiers. Use SendBulkEmail with batched destinations and throttle at the SDK or queue layer.
Engagement-based filtering. The single highest-leverage change most teams make post-migration is to stop sending to disengaged subscribers entirely. Maintain a “send eligibility” flag in your subscriber store, updated nightly from SES open and click events, and exclude anyone who has not engaged in your defined window. Gmail and Microsoft weight recent positive engagement (opens, replies, “move to inbox”) far more heavily than total volume, so a smaller, hotter list lifts inbox placement for the entire domain — typically from the mid-80s to the mid-90s within 30 days.
Production Event Tracking: SES → Kinesis Firehose → S3 → Node.js
Mailgun gives you a polished event log with 7 days of retention on Foundation and 30 days on Flex/Growth. After cutover to SES, the SNS → Lambda pattern from the webhook section above handles tens of thousands of events per day. Past that volume, or when you need historical analytics that outlive Mailgun’s hosted window, the production-grade pipeline is SES → Kinesis Data Firehose → S3 → Node.js API.
How it works.
- SES Configuration Set — define one per sending stream (transactional, marketing, lifecycle) and attach a Kinesis Firehose event destination. Subscribe to all event types:
send,delivery,bounce,complaint,open,click,reject,renderingFailure,deliveryDelay, andsubscription. - Kinesis Data Firehose — buffers events (typically 60 seconds or 5 MB) and writes newline-delimited JSON to S3 partitioned by
year/month/day/hour. Enable dynamic partitioning to split by Configuration Set or sending IP for downstream filtering. - S3 with lifecycle policy — Standard for 30 days (hot analytics window), Standard-IA at 30 days, Glacier Flexible Retrieval at 180 days. Raw event archives are the cheapest part of the stack and the most useful during deliverability investigations.
- Node.js API — consume events by tailing new S3 objects via S3 Event Notifications → SQS → Node.js worker (Express or Fastify behind an ALB, or a Lambda function for spiky traffic), or query historical data via Amazon Athena. The API exposes per-recipient timelines, per-campaign engagement, and per-domain placement metrics to your dashboards.
Why this beats Mailgun’s hosted event log.
- Retention. S3 with Glacier transition gives you years of event history for a few dollars a month. Mailgun’s hosted log capped retroactive analytics at 7–30 days; you can do better.
- Replayable. S3 is the source of truth. Rebuild a recipient timeline, re-run a deliverability analysis, or backfill a new dashboard without re-fetching from a vendor API.
- Cheap analytics. Athena over partitioned S3 answers “what was my Gmail open rate by sending IP last week?” in seconds, not engineering tickets.
- Audit trail. Compliance and abuse investigations require raw event history. Firehose-to-S3 gives it to you immutably.
Filtering bots, proxies, and Apple MPP. SES open events fire on pixel load and click events fire on the SES tracking redirector. Both are noisy: corporate proxies, link-scanning gateways, and Apple Mail Privacy Protection pre-fetch images and follow links, generating engagement signals that have nothing to do with a human reading the message. Without filtering, your “open rate” double-counts machine activity and your engagement-based suppression logic ends up suppressing real subscribers.
InboxEagle’s Bot Finder sits naturally between the S3 raw layer and the engagement-based filtering layer described above — it analyzes SES open and click streams, separates human engagement from bot and proxy activity (Apple MPP, Microsoft Defender, Gmail image proxy, link-scanning gateways), and feeds your Node.js API cleaned engagement data so your reputation signals reflect real subscriber behavior. Teams migrating off Mailgun’s pre-aggregated event UI underestimate how much inbox-placement signal lives in distinguishing real opens from scanner activity; a bot filter is the difference between an engagement model that improves placement and one that quietly degrades it.
Why Choose FactualMinds for Your Email Migration
FactualMinds is an AWS Select Tier Consulting Partner specializing in email infrastructure migration. We have executed SendGrid, Mailgun, Postmark, and SparkPost to AWS SES migrations and know exactly where teams get stuck.
- Email migration experts — we handle domain verification, DKIM, bounce architecture, IP warming
- Assessment-first approach — we map your current state before writing a line of infrastructure code
- Zero-downtime cutover planning included — no failed deliveries during migration
- AWS Select Tier Partner — verified on AWS Partner Network
Frequently Asked Questions
How does Mailgun compare to AWS SES?
Can AWS SES receive email like Mailgun?
Is Mailgun or AWS SES better for transactional email?
How do I migrate Mailgun suppression lists to SES?
What is the equivalent of Mailgun routing in AWS SES?
Need Help Migrating to AWS SES?
FactualMinds is an AWS Select Tier Partner specializing in email infrastructure migration. We handle domain verification, Configuration Set architecture, bounce handling, IP warming, and cutover.
