email · side by side
Mailgun vs Resend for a Next.js app
Both fill the email slot, so a generated repo carries one or the other, never both. Every line below is read out of the two manifests.
Short answer
Pick Mailgun if
Teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun.
Pick Resend if
Product teams sending transactional mail: magic links, receipts, notifications. Templates are React components, reviewed in a pull request, not pasted into a vendor's WYSIWYG editor.
Side by side
Price, obligations, and the surface each one adds. No row is written by hand. This is manifest.yaml, rendered.
| From the manifest | Option AMailgun | Option BResend |
|---|---|---|
| In one line | Mailgun The veteran sending API. EU data residency and suppression lists kept server-side. | Resend Transactional email with React Email templates you review in a pull request. |
| Pricing | Mailgun Free plan: 100 emails a day and 1 custom domain. Basic starts at $15/month for 10,000 emails. Foundation is $35/month for 50,000 and Scale $90/month for 100,000. Logs are kept 1 day on Free and Basic, 5 days on Foundation and 30 days on Scale. | Resend Free: 3,000 emails a month (100 a day) on up to 3 domains. Pro is $20/month for 50,000 emails and 10 domains. Every self-serve plan keeps 30 days of data. Dedicated IPs are a $30/month add-on on Scale. |
| Best for | Mailgun Teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun. | Resend Product teams sending transactional mail: magic links, receipts, notifications. Templates are React components, reviewed in a pull request, not pasted into a vendor's WYSIWYG editor. |
| Trade-offsVerbatim from the manifest | Mailgun
| Resend
|
| Required companionsAdded for you, with a reason | Mailgun Nothing. It stands on its own. | Resend Nothing. It stands on its own. |
| Recommended alongsideSuggested, never added for you | Mailgun Nothing suggested. | Resend
|
| Env vars you will manageEvery one documented in docs/onboard.md | Mailgun 7 variables · 3 required
| Resend 5 variables · 2 required
|
| Dependencies added | Mailgun
| Resend
|
| MCP serversWritten into .mcp.json | Mailgun None. No extra agent tools from this one. | Resend None. No extra agent tools from this one. |
| Footprint in your repo | Mailgun 22 files, plus 3 injections into shared stack files | Resend 26 files, plus 3 injections into shared stack files |
What changes in your repo
The paths each battery contributes, diffed. A path in the third list is written by both, so swapping rewrites that file rather than adding one.
Only with Mailgun (4)
scripts/1 file
email/1 file
- suppressions.ts
src/3 files
app/1 file
api/1 file
webhooks/1 file
mailgun/1 file
- route.ts
lib/2 files
email/2 files
templates/1 file
- layout.tsx
- mailgun.ts
Only with Resend (8)
src/2 files
app/1 file
api/1 file
webhooks/1 file
resend/1 file
- route.ts
lib/1 file
email/1 file
- resend.ts
variants/6 files
orm-drizzle/3 files
slots/1 file
- db-schema.ts
src/2 files
db/1 file
- email-schema.ts
lib/1 file
email/1 file
- store.ts
orm-none/1 file
src/1 file
lib/1 file
email/1 file
- store.ts
orm-prisma/2 files
slots/1 file
- prisma-models.prisma
src/1 file
lib/1 file
email/1 file
- store.ts
Same path, different implementation (18)
scripts/2 files
email/2 files
- render-samples.ts
- send-test.ts
src/12 files
lib/12 files
email/12 files
templates/6 files
- magic-link.tsx
- receipt.tsx
- reset-password.tsx
- theme.ts
- verify-email.tsx
- welcome.tsx
- address.ts
- index.ts
- outbox.ts
- retry.ts
- suppression.ts
- tags.ts
tests/2 files
unit/2 files
- email-outbox.test.ts
- email.test.ts
variants/2 files
errors-none/1 file
src/1 file
lib/1 file
email/1 file
- observability.ts
errors-sentry/1 file
src/1 file
lib/1 file
email/1 file
- observability.ts
Shared stack files Mailgun injects into
- env-required
- legal-processors
- verify-checks
Shared stack files Resend injects into
- env-required
- legal-processors
- verify-checks
Mailgun in your .env.local
# required
EMAIL_FROM=my-app <hello@mail.example.com>
MAILGUN_API_KEY=key-replace_me
MAILGUN_DOMAIN=mail.example.com
# optional
EMAIL_OUTBOX_DIR=
MAILGUN_REGION=us
MAILGUN_WEBHOOK_SIGNING_KEY=whsk-replace_me
REPLY_TO=support@example.com
Resend in your .env.local
# required
EMAIL_FROM=my-app <hello@mail.example.com>
RESEND_API_KEY=re_replace_me
# optional
EMAIL_OUTBOX_DIR=
REPLY_TO=support@example.com
RESEND_WEBHOOK_SECRET=whsec_replace_me
What changes for your agents
Each battery ships rules, skills, subagents and hooks that an agent loads before it touches the code that battery owns. Picking one is also picking how your agents behave in src/lib/email/**.
Mailgun
3
Skills
1
Rules
5
Solution docs
Rules (1)
One send function, a verified domain, a reply-to, and no secrets in the body
src/lib/email/** · src/app/api/webhooks/mailgun/** · src/lib/auth/** · src/lib/billing/**
Skills (3)
/add-email-template
Add a React Email template on the shared layout, preview it, wire it into a sendEmail call, and confirm it renders as HTML and as text in a real inbox.
/diagnose-email-delivery
Work out why a Mailgun message did not arrive (region, key, suppression, authentication or placement) in the order that finds it fastest.
/test-email
Prove a send actually works: check the region and domain, send one of each template, read the Mailgun logs, inspect the suppression lists and confirm placement in a real inbox.
Subagents and hooks
None of its own. The foundation agents and guard hooks still ship.
Resend
2
Skills
1
Rules
5
Solution docs
Rules (1)
Every email goes through sendEmail, from a verified domain, with a reply-to
src/lib/email/** · src/app/api/webhooks/resend/** · src/lib/auth/** · src/lib/billing/**
Skills (2)
/add-email-template
Add a React Email template, preview it, wire it into a send, and check it renders and lands in a real inbox.
/preview-and-test-email
Diagnose an email problem (not sending, landing in spam, rendering wrong) in the order that finds the cause fastest.
Subagents and hooks
None of its own. The foundation agents and guard hooks still ship.
What each one already knows
Solution docs land in docs/solutions/ in your repo and are published here, so you can read the failure modes before you commit.
Mailgun (5)
- Mailgun returns 401 and your API key is fine: the EU/US region trapA Mailgun domain lives in the region it was created in. Calling the wrong regional host returns 401 Unauthorized, which reads exactly like a bad API key.docs/solutions/mailgun/eu-vs-us-region-endpoints.md
- Mailgun says the message was queued and nobody receives it: sandbox domain limitsThe sandbox domain accepts every send and delivers only to five addresses you authorised. Recognise it, use it deliberately, and know when to stop.docs/solutions/mailgun/sandbox-domain-limits.md
- Suppression lists in Mailgun: stop maintaining your own bounce tableMailgun stores bounces, complaints and unsubscribes per domain and refuses to send to them. Read that list instead of building a table you have to keep in sync.docs/solutions/mailgun/suppression-lists.md
- Mailgun templates and recipient variables versus rendering HTML in your appMailgun stores Handlebars templates in its dashboard and substitutes variables at send time. Rendering in your app instead keeps email in code review: here is when each one wins.docs/solutions/mailgun/template-variables-vs-rendered-html.md
- Open tracking, click tracking and why they are off by defaultMailgun's tracking adds a pixel and rewrites every link. Both are personal data under GDPR, both cost deliverability, and neither belongs on a magic link.docs/solutions/mailgun/tracking-pixels-and-privacy.md
Resend (5)
- Sending a lot of email without hitting the rate limit or the spam folderResend allows a couple of requests a second by default. Use the batch endpoint, add backoff for 429s, keep one idempotency key per recipient, and never batch a magic link.docs/solutions/resend/batching-and-rate-limits.md
- Bounces and spam complaints: listen, or lose the inbox for everyoneA hard bounce means the mailbox is gone. Keep sending and mailbox providers downgrade every message from your domain. Wire the webhook, suppress permanently, and never suppress on a soft bounce.docs/solutions/resend/bounces-complaints-and-webhooks.md
- SPF, DKIM and DMARC: what each record does and why your mail needs all threeThree DNS records decide whether a mailbox provider treats your email as authentic. Here is what each one proves, how to set them up on a subdomain, and how to read a failure.docs/solutions/resend/domain-verification-spf-dkim-dmarc.md
- Magic links that actually arrive, and survive the scanner that clicks them firstA sign-in link is the highest-stakes email you send. It has to land in seconds, work once, and survive corporate mail scanners that follow every URL before the human does.docs/solutions/resend/magic-link-deliverability.md
- Previewing React Email templates locally, and what the preview cannot tell youReact Email's preview server renders your templates with realistic props and hot reload. Here is how to set it up, what to check, and the four failure modes only a real client will show you.docs/solutions/resend/previewing-templates-locally.md
Which one to pick
From meta.bestFor and meta.tradeoffs. If a claim is not in the manifest, it is not on this page.
Pick Mailgun when
Teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun.
And accept that(5)
- Region is chosen when the domain is created and cannot change. An EU domain must use api.eu.mailgun.net. The US host returns a 401 that reads like a bad key.
- The API is form-encoded, not JSON, which is why the SDK needs form-data as a peer. It is an older API and it shows in places.
- No React rendering on Mailgun's side. Templates are rendered to HTML in your app before sending. Mailgun's own templates are Handlebars stored in the dashboard, a different workflow this battery does not use.
- No built-in idempotency key. A retried job can send twice unless you deduplicate, so this battery attaches a deterministic Message-Id and a custom variable.
- Suppression lists live in Mailgun and are queryable through the API. No local table to maintain and one less thing to keep in sync.
Pick Resend when
Product teams sending transactional mail: magic links, receipts, notifications. Templates are React components, reviewed in a pull request, not pasted into a vendor's WYSIWYG editor.
And accept that(5)
- Transactional first. There is a broadcast product, but if you need segmentation, drip campaigns and a marketing CRM, this is not that tool.
- Shared IPs by default. Dedicated IPs are a paid add-on on the Scale plan, so on the shared pool your own sending hygiene matters more.
- Data is kept 30 days. If you need to prove what you sent someone six months ago, log the message id at send time. The repo does.
- React Email is the headline feature and a real one: templates are components, previewable locally, diffable in review. It also means your email templates are part of your build.
- Fewer knobs than a full ESP: no server-side template variables, no A/B testing, limited routing rules. For transactional mail that is a simpler model, not a gap.
Questions people actually ask
- Should I choose Mailgun or Resend?
- Mailgun is best for teams that need EU data residency, or want suppression and analytics held by the provider instead of their own database. Also teams sending enough volume to care about dedicated IPs and warm-up support. And the pragmatic choice when an existing system already speaks Mailgun. Resend is best for product teams sending transactional mail: magic links, receipts, notifications. Templates are React components, reviewed in a pull request, not pasted into a vendor's WYSIWYG editor. Both fill the email slot, so a generated repo carries one or the other, never both.
- How much do Mailgun and Resend cost?
- Mailgun: Free plan: 100 emails a day and 1 custom domain. Basic starts at $15/month for 10,000 emails. Foundation is $35/month for 50,000 and Scale $90/month for 100,000. Logs are kept 1 day on Free and Basic, 5 days on Foundation and 30 days on Scale. Resend: Free: 3,000 emails a month (100 a day) on up to 3 domains. Pro is $20/month for 50,000 emails and 10 domains. Every self-serve plan keeps 30 days of data. Dedicated IPs are a $30/month add-on on Scale.
- What changes in my repo if I switch from Mailgun to Resend?
- Mailgun writes 22 files, 7 environment variables and 5 dependencies, and installs 1 path-scoped rule, 3 skills and 5 solution docs. Resend writes 26 files, 5 environment variables and 5 dependencies, and installs 1 path-scoped rule, 2 skills and 5 solution docs.
Decide once, then build the repo that already knows the decision.
Either way you get that choice’s rules, skills and solution docs installed, plus the guard hooks, an onboarding doc for exactly these env vars, and the Compound Engineering loop. Free and MIT.