Skip to main content

Resend to AWS SES Migration

Migrating from Resend to AWS SES: A Practical Guide

Resend earned its reputation on a clean SDK, React Email, and a developer experience that makes 5,000-message-per-month senders feel like first-class citizens. Once volume climbs past the included Pro tier or AWS consolidation enters the picture, SES becomes the next stop. The full migration runbook — React Email portability, Audiences and Broadcasts replacements, the SES event pipeline you will own, and the deliverability discipline that keeps inbox placement steady through the cutover.

Ask AI: ChatGPT Claude Perplexity Gemini

TL;DR. If you are migrating only transactional, plan 1–2 weeks on the SES side and keep React Email as-is. If you also need to replace Audiences and Broadcasts, plan an additional 1–2 engineering weeks for the contact store + bounce-sync layer described below.

Resend launched in 2023 with a clear bet: developers writing React in 2026 want to write email in React, see template previews instantly, and ship from a typed SDK without thinking about MIME boundaries or SMTP timeouts. The bet paid off — Resend is the email vendor of choice for new SaaS teams, indie shipped projects, and AI-era startups that grew up on Vercel and Linear. AWS SES is where those teams move when the platform engineering function arrives, when finance starts asking about per-vendor spend, or when a parent product on AWS consolidates infrastructure under one cloud.

This guide is written for the engineer running that migration.

Where Resend and SES Sit in the Market

Resend is a managed email API designed around modern developer experience. It ships with an opinionated React Email integration, a polished dashboard, generous tooling, and pricing that targets early-stage teams. Operationally it sits on top of multiple sending backends — AWS SES is publicly disclosed as one of them — and abstracts the sending pool, IP rotation, and feedback loops away from you.

AWS SES is a raw email-sending service. There is no template editor, no dashboard analytics beyond reputation metrics, no contact list manager, and no campaign scheduler. What you get is a high-throughput send API, configuration sets for routing and tagging, an event firehose, and IAM-controlled access. The product is a primitive; the value comes from what you build around it.

The migration is fundamentally about deciding to own more of the stack in exchange for control, observability, and lower per-message cost.

The Pricing Curve

Resend’s pricing is designed for two segments: free for early development, Pro for shipped products under 50,000 sends per month, then a step into marketing-tier pricing for senders running broadcasts.

VolumeResendAWS SESMonthly Difference
3,000 emails$0 (Free tier)$0.30Resend +$0.30
50,000 emails$20/month (Pro)$5.00/month$15.00 SES savings
100,000 emails~$45/month (Pro + overage)$10.00/month$35.00 SES savings
500,000 emails~$200/month (custom tiers)$50.00/month$150.00 SES savings
1,000,000 emails~$350/month (custom tiers)$100.00/month$250.00 SES savings

At low volume the cost gap is small enough that Resend’s developer experience pays for itself. The crossover where SES becomes obvious typically lands somewhere between 200,000 and 500,000 messages per month — the volume where engineering time to build event pipelines and broadcast tooling is recovered within one to two quarters of saved spend.

Two non-pricing drivers usually accelerate the decision:

API Migration: Resend SDK → AWS SES SDK

The Resend SDK and the AWS SDK for SES are both well-typed and idiomatic. The shape of a send call is conceptually identical; only the surface changes.

ResendAWS SES EquivalentNotes
resend.emails.send()SendEmailCommand via @aws-sdk/client-sesCore send operation. Same from/to/subject shape.
react: <Email /> shorthandrender(<Email />) from @react-email/renderRender in app code, pass HTML and text to SES.
tags: [{ name, value }]Tags: [{ Name, Value }]Direct mapping. Used for CloudWatch breakdowns and event filtering.
headers: { 'X-Entity-Ref-ID': '...' }Headers array on SendEmailCommandCustom headers pass through unchanged.
attachments: [{ filename, content }]SendRawEmailCommand with MIME-encoded attachmentSES requires the raw MIME path for attachments. Use Nodemailer or mimetext to assemble.
API key in Authorization: Bearer headerIAM credentials via SDK or SES SMTP credentialsPrefer IAM roles in production (no key rotation).
Webhook signing via Svix-Signature headerSNS message signature verificationSNS subscriptions sign messages; verify with the AWS SDK helper.
resend.emails.batch.send() (up to 100)SendBulkEmailCommand (up to 50 destinations)Loop and batch for larger lists.

A typical Resend-to-SES code change for a transactional send:

// Before — Resend
import { Resend } from 'resend';
import VerifyEmail from './emails/verify';

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: 'Acme <hello@acme.com>',
  to: user.email,
  subject: 'Verify your email',
  react: <VerifyEmail name={user.name} url={verifyUrl} />,
  tags: [{ name: 'category', value: 'verification' }],
});

// After — AWS SES
import { render } from '@react-email/render';
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import VerifyEmail from './emails/verify';

const ses = new SESClient({ region: 'us-east-1' });
const html = await render(<VerifyEmail name={user.name} url={verifyUrl} />);
const text = await render(<VerifyEmail name={user.name} url={verifyUrl} />, {
  plainText: true,
});

await ses.send(
  new SendEmailCommand({
    Source: 'Acme <hello@acme.com>',
    Destination: { ToAddresses: [user.email] },
    Message: {
      Subject: { Data: 'Verify your email' },
      Body: {
        Html: { Data: html },
        Text: { Data: text },
      },
    },
    ConfigurationSetName: 'transactional',
    Tags: [{ Name: 'category', Value: 'verification' }],
  })
);

The SES version is more verbose, but every concept maps one-to-one. React Email components, props, and styling are unchanged.

React Email Stays — That Is the Point

The single biggest concern engineers raise about leaving Resend is template portability. React Email has no Resend lock-in. The library is open source, owned by the Resend team but designed from day one to be provider-agnostic. Components, layouts, Tailwind classes, dark mode handling, and plain-text rendering all continue to work after the migration.

What you keep:

What you lose:

A clean pattern is to put a thin sendEmail() helper in your shared infrastructure module that wraps the SES SDK and accepts a React Email component. Every send site in the application then looks like sendEmail({ to, subject, component: <VerifyEmail ... /> }), and you have a single place to add tagging, idempotency, and suppression logic.

Replacing Resend Audiences and Contacts

Resend Audiences holds your contact list and tracks subscribe/unsubscribe state. SES has no equivalent — you build the contact store yourself. The model is small and the build is one to two engineering weeks.

A working contact store on AWS for a team in the 100K–1M sends-per-month range:

Migration data flow:

  1. Export each Resend Audience via the Resend API (GET /audiences/:id/contacts)
  2. Import into DynamoDB with status preserved (subscribed, unsubscribed, etc.)
  3. Verify your unsubscribe URLs in existing emails resolve to the new endpoint or set up a temporary redirect
  4. Cut over send code to read from the new contact store

Building this is one to two engineering weeks for a focused team — meaningfully more if you also need a marketing-team-friendly admin UI to manage contacts, in which case you should evaluate whether keeping marketing on Resend (or a marketing-specific platform like Customer.io or Loops) and only moving transactional to SES is the right architecture.

Replacing Resend Broadcasts

Broadcasts are scheduled, audience-targeted email campaigns with engagement reporting. The SES equivalent is a small orchestration layer:

EventBridge Scheduler (cron-style trigger)

Step Functions state machine
  ↓ Distributed Map over audience contacts (parallelism: 100)

Lambda (per-contact send)
  ↓ render React Email → SendBulkEmailCommand → SES
SES → recipient

SES Configuration Set → Kinesis Firehose → S3 (raw events)

Athena queries → broadcast_id → engagement metrics

Step Functions Distributed Map is the right primitive — it parallelizes the per-contact send up to 10,000 concurrent executions and gives you per-batch retry semantics out of the box. Compared to a hand-rolled SQS-driven sender, you get a visual execution view, structured failure handling, and a single execution ARN per broadcast that ties together logs, metrics, and replay.

Per-broadcast engagement reporting (open rate, click rate, bounce rate, complaint rate) comes from tagging every send with a broadcast_id Configuration Set tag, then aggregating SNS or Firehose events filtered by that tag. The result is a Postmark-style or Resend-style activity view that you own end-to-end.

Domain Verification, DKIM, and the Authentication Stack

Resend handles domain verification through a simple “add these DNS records” UI and signs all outbound mail with their managed DKIM keys. SES asks you to do the same work but exposes more knobs. In 2026 — Gmail, Yahoo, and Microsoft now treat unauthenticated mail from any sender above 5,000 messages per day as effectively undeliverable — getting authentication right before the first production send is non-negotiable.

SPF. Add include:amazonses.com to your sending domain’s TXT record. If you keep Resend running during the cutover window, your record looks like v=spf1 include:_spf.resend.com include:amazonses.com -all until you fully cut over. Watch the 10-DNS-lookup limit; chained include: directives silently break SPF and are the most common authentication regression during a phased migration.

DKIM. Verify the domain in SES, enable Easy DKIM, and publish the three CNAME records SES generates. Easy DKIM rotates keys automatically. During parallel sending, Resend’s DKIM selector and SES’s three CNAMEs coexist without conflict — both signatures validate independently.

DMARC. Publish _dmarc.yourdomain.com with v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com from 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, calendar invites, vendor notifications — is aligned. Gmail and Yahoo’s bulk-sender requirements made enforcement mandatory; p=none meets the minimum but only enforcement actually blocks spoofing.

Custom MAIL FROM domain. Set up a custom MAIL FROM subdomain (e.g., mail.acme.com) and publish the SES-provided MX and SPF records for it. This makes SPF alignment with your visible From address explicit, which removes a class of subtle DMARC failures that surface only after p=reject enforcement.

BIMI. Once enforcement is stable and you have a Verified Mark Certificate from Entrust or DigiCert, publish a BIMI record so your logo renders next to messages in Gmail, Apple Mail, and Yahoo. BIMI does not directly improve placement but the trust signal lifts open rates measurably for transactional and lifecycle email.

Step-by-Step Migration Plan

A clean Resend-to-SES cutover for a single product domain takes one to three weeks of focused engineering work. The phases below assume you are migrating both transactional and marketing traffic; pure transactional cutover is the first three phases only.

Phase 1 — Inventory and parallel infrastructure (Week 1)

  1. Audit every send site in the codebase. Group by send category — transactional (verification, password reset, MFA, receipts), product activity (notifications, mentions), marketing (broadcasts, newsletters).
  2. Verify your sending domain in SES. Publish DKIM and SPF records. Leave Resend records in place.
  3. Create one Configuration Set per send category. Wire each to an SNS topic (or directly to Kinesis Firehose, see below). Tag every Configuration Set with category.
  4. Move out of the SES sandbox by submitting a production access request. Expect 24–48 hours.
  5. Stand up the contact store (DynamoDB + Lambda) if you used Resend Audiences.

Phase 2 — Send code refactor (Week 1–2)

  1. Wrap every send call site behind a sendEmail() helper that accepts a React Email component, recipient, subject, and category.
  2. Add a feature flag (e.g., EMAIL_PROVIDER=resend|ses|both) so you can route by category. Start sending non-critical categories (internal alerts, low-value notifications) through SES first.
  3. Render React Email components in application code. Verify the rendered HTML matches what Resend was producing. Pixel-perfect parity is the bar for transactional templates.
  4. Implement the per-send checks: account-level suppression cache, per-category preference store, frequency cap, idempotency key.

Phase 3 — Event pipeline (Week 2)

  1. Wire every Configuration Set to Kinesis Data Firehose, partition by year/month/day/category in S3.
  2. Build the smallest viable replacement for the Resend dashboard: a Lambda or Node.js API that queries Athena for recipient timeline, per-category metrics, bounce and complaint detail.
  3. Add CloudWatch alarms on bounce rate (>2%), complaint rate (>0.1%), and send-rate anomalies (10x deviation from 7-day rolling average).
  4. Verify SNS subscription signature on every webhook handler. Resend used Svix; SES uses SNS message signing — different validation library, same security guarantee.

Phase 4 — Marketing migration (Week 2–3, optional)

  1. Build the broadcast orchestration layer (EventBridge Scheduler + Step Functions Distributed Map).
  2. Migrate Audience data from Resend export to DynamoDB.
  3. Run a small broadcast (1,000 recipients) end-to-end to validate sending, tracking, and unsubscribe handling.
  4. Scale up gradually over 7–10 days, monitoring per-domain placement.

Phase 5 — Cutover and decommission (Week 3+)

  1. Flip the feature flag for transactional traffic. Keep Resend running for 24–48 hours as fast rollback.
  2. Watch CloudWatch dashboards and Resend dashboards in parallel. Investigate any divergence immediately.
  3. After 7 clean days, remove Resend SDK from dependencies, rotate the API key, and remove Resend’s SPF include from DNS.

Common Migration Challenges

Attachment encoding. Resend’s SDK accepts attachments as Base64 strings or buffers; SES SendEmail does not support attachments at all. You must use SendRawEmailCommand with a MIME-encoded message body. Use mimetext or Nodemailer’s compose function to assemble the raw message — do not roll your own MIME serialization.

Webhook signature drift. Resend signs webhooks with Svix. SES signs SNS messages with X.509 certificates served from *.amazonaws.com. The SDK helper verifyMessageSignature validates incoming SNS notifications; do not skip the verification step. Public SNS HTTP endpoints with weak validation are an active target for spoofed bounce-event attacks that poison your suppression list.

Apple MPP open inflation. Resend’s dashboard reports raw open events. After migration to SES, your event pipeline shows opens too — but Apple Mail Privacy Protection prefetches every image in a recipient’s inbox, inflating the open rate by 20–60% with non-human signal. Filter MPP opens (identifiable by the User-Agent: Mail/MPP and Apple-owned IP ranges) before feeding engagement data into segmentation logic.

Suppression list import latency. Resend exposes hard-bounced and complained addresses through their API but historical data is limited. Export everything you can on the day of cutover and bulk-load it into the SES account-level suppression list via PutSuppressedDestination. Anything not on the list will hard-bounce and rebuild reputation damage before SES auto-suppresses.

Rate limits and concurrent send patterns. Resend’s send API tolerates bursts well. SES enforces a per-second send rate (MaxSendRate) tied to your account quota. Code that fired 1,000 sends in parallel against Resend will throttle on SES until the quota grows. Smooth bulk sends through SQS or SendBulkEmail rather than Promise.all over a contact list.

Domain verification across regions. Resend is region-agnostic from your perspective. SES verification is per-region — verifying acme.com in us-east-1 does not verify it in eu-west-1. If you split sending across regions for latency or data residency, publish DKIM and DMARC records for each region separately.

Deliverability Discipline After the Cutover

Resend operates the sending pool, the feedback loops, and the throttling for you. After migration, all three become your responsibility. Three operational habits separate teams that maintain Resend-grade inbox placement on SES from teams that watch their open rate decay over six months.

Stream isolation. Run separate Configuration Sets — and ideally separate dedicated IP pools — for transactional, product activity, and marketing traffic. A complaint spike on a marketing broadcast cannot reach password-reset deliverability if the IPs are isolated. Application code selects the correct Configuration Set per send category; tag every send with category for downstream filtering.

Engagement-based send eligibility. Maintain a last_engaged_at timestamp per recipient updated nightly from open and click events. Suppress recipients with no engagement in 90 days from marketing streams (180 days for transactional). Gmail and Microsoft weight recent positive engagement heavily — a smaller, hotter list lifts placement for the entire domain, typically from the mid-80s to the mid-90s within 30 days.

Bot and prefetch filtering. Apple MPP, Microsoft Defender link scanning, Gmail image proxies, and corporate security gateways all generate engagement events that have nothing to do with a human reading the message. Filter these before feeding signals into engagement-based suppression logic; without filtering, engagement-based suppression suppresses your real subscribers.

Per-domain placement testing. Send to a small panel of monitored seed inboxes (Gmail, Outlook, Yahoo, iCloud) on every major broadcast. Inbox vs. promotions vs. spam folder placement is your earliest warning system — much earlier than the bounce rate metric, which only spikes after reputation damage is already done.

Production Event Pipeline: SES → Kinesis Firehose → S3 → Node.js API

Resend’s hosted dashboard gives you a clean event view with searchable activity logs. After cutover you need to replicate that surface to keep observability parity. The architecture most production SES senders converge on is streaming the SES event firehose into S3 and exposing a thin query API.

SES Configuration Set
   ↓ (event destination)
Kinesis Data Firehose
   ↓ (60-second buffer or 5 MB)
S3 (Parquet, partitioned by year/month/day/category)

Athena   ←  Node.js API   ←  Dashboard / suppression service / Slack alerts

Subscribe each Configuration Set to a Firehose delivery stream. Enable dynamic partitioning so events split by category, sending IP, or domain at write time. Lifecycle the bucket: Standard for 30 days (hot analytics window), Standard-IA at 30 days, Glacier Flexible Retrieval at 180 days. Raw events are the cheapest part of the stack and the most useful during deliverability investigations.

The Node.js API layer is small — a Fastify or Hono service, an Athena query helper, and four to six endpoints:

Filtering bot and prefetch traffic at this layer (or upstream of it) is what makes the engagement metrics actionable. Open and click events from corporate security scanners and image proxies should be tagged automated and excluded from engagement-based suppression decisions; otherwise, engagement-based suppression gradually suppresses real subscribers whose mail clients prefetch links. Tools that score SES events for bot vs. human activity in real time fit naturally between the Firehose stream and the API layer; teams that skip this filtering often discover the gap only after a quarter of degraded marketing placement.

Explore other technical comparisons:

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, SparkPost, and Resend to AWS SES migrations and know exactly where teams get stuck.


Frequently Asked Questions

Why would I leave Resend for AWS SES?
The two common triggers are price and platform consolidation. Resend's Pro plan covers 50,000 emails per month at $20, then steps to roughly $90 for 100,000 marketing-eligible emails — meaningfully more than SES's flat $0.10 per thousand once you cross 100K. Beyond pricing, teams already running production workloads on AWS prefer to consolidate billing, IAM, and observability under one cloud. Resend is excellent for the first one to two years of a SaaS — fast to integrate, well-documented, no AWS account paperwork — but the AWS-native control surface (IAM, KMS, VPC endpoints, CloudWatch) becomes the deciding factor once your platform engineering team is in place.
Does Resend run on AWS SES under the hood?
Resend has been transparent that AWS SES is one of the upstream providers in their multi-vendor sending stack. That is good news for migration: the underlying deliverability mechanics are similar, and IPs that send your Resend mail today and the SES IPs you will use after migration sit on similar reputation infrastructure. The catch is that the IPs and reputation that delivered your Resend traffic are not yours — they belong to the Resend pool. After cutover you start cold on either the SES shared pool (suitable for most senders) or your own dedicated IPs (which require warming). Plan IP-warming the same way you would migrating from any other provider.
How do I migrate React Email templates from Resend to AWS SES?
React Email is provider-agnostic — it renders MJML or HTML from React components and has no Resend-specific dependencies. The migration is changing the SDK call. Replace `resend.emails.send({ react: <Email /> })` with the `@react-email/render` helper and `@aws-sdk/client-ses` `SendEmailCommand`. Render the React component to HTML and plain text in your application code, then pass both to SES. Templates, components, and previews all continue to work. The only Resend-specific thing to remove is `Resend` SDK imports; everything else — `Tailwind`, `Section`, `Container`, `Button` from `@react-email/components` — is portable.
What is the SES equivalent of Resend Audiences and Broadcasts?
There is no native equivalent. Resend Audiences (contact lists with subscribe/unsubscribe handling) and Broadcasts (scheduled campaign sends with engagement reporting) are application-layer features built on top of the send API. To replicate them on SES you store contacts and preferences in DynamoDB, RDS, or Postgres, build a small admin surface for subscribe and unsubscribe state, and trigger sends from EventBridge Scheduler or Step Functions. For most teams this is a one-time engineering investment that pays back in a few months versus the per-message cost difference. Teams that primarily rely on Resend for marketing broadcasts and lack engineering bandwidth often stay on Resend for marketing while moving transactional traffic to SES.
How does Resend pricing compare to AWS SES at scale?
Resend Free covers 3,000 emails per month and 100 per day — fine for prototypes. Pro at $20 per month covers 50,000 emails. Beyond that, Resend's marketing-tier pricing scales to roughly $90 for 100,000 broadcast emails and custom-priced beyond. SES is flat at $0.10 per 1,000 emails with no plan tier — $5 for 50K, $10 for 100K, $100 for 1M. At 50,000 emails per month the savings are modest ($15 per month). At 1,000,000 the gap is in the hundreds. The cost calculus is rarely the only factor at small volume; AWS-native integration, IP control, and event pipeline ownership matter more for teams sending past 200,000 messages per month.

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.