On May 27, 2025, Twilio killed SendGrid's permanent free tier. New accounts get a 60-day trial, then paid plans start at $19.95/month. Thousands of SaaS founders woke up to an email telling them the thing they'd been sending password resets and welcome emails through for years now had an expiration date.
Pricing was just the trigger, though. The frustrations had been piling up for a while: a proprietary template editor you can't version control, Handlebars syntax with zero type safety, and a debugging workflow that amounts to squinting at raw HTML in a browser tab. Losing the free tier gave teams the push to finally deal with all of it.
This post walks through what that migration to Resend and React Email looks like in practice, what changes at each layer, and where teams burn the most time when they try to rebuild every template from a blank file.
We spent three weeks hand-coding 14 email templates and testing them across Outlook, Gmail, and Apple Mail. Every fix for one client broke another. Then someone found a pre-built React Email library and we shipped the rest of the migration over a weekend.
Indie SaaS dev
r/SaaS, March 2026
What actually changes in the migration
Most people assume this is an API key swap. It's not. Four layers need to change, and skipping any one of them causes problems once you're live.
| Layer | SendGrid | Resend + React Email | Migration Effort |
|---|---|---|---|
| DNS / Authentication | SPF, DKIM via SendGrid dashboard | SPF, DKIM via Resend dashboard | 30 minutes - just new DNS records |
| API Integration | @sendgrid/mail SDK, REST API | resend SDK, one-line sends | 1-2 hours per endpoint |
| Templates | Dynamic Templates (Handlebars in web UI) | React components with TypeScript props | 2-5 hours per template from scratch |
| Deliverability | IP warm-up via SendGrid | Shared IPs (pre-warmed) or dedicated | Zero if using shared IPs |
The four layers of a SendGrid to Resend migration
The migration timeline
Based on what we've seen from teams making this switch in early 2026, a typical migration follows a predictable pattern. The first two days feel fast. Then the templates hit.
DNS and domain verification
API integration swap
@sendgrid/mail calls with the Resend SDK. The API surface is much smaller: one function, typed parameters, no template IDs to manage.Template rebuild (the bottleneck)
Testing and cutover
Monitoring
The shortcut most teams miss
Days 3-5 are the bottleneck. Teams that use production-ready React Email templates compress that three-day stretch into a few hours of customization instead of building from a blank file. The template layer is the only part of this migration that doesn't have to be built from scratch.
The API swap: SendGrid vs Resend
The API change is the easiest part. A welcome email send in both providers shows the difference clearly.
SendGrid: template ID + substitutions
import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
export async function sendWelcomeEmail(
to: string,
name: string,
plan: string
) {
await sgMail.send({
to,
from: "hello@yourapp.com",
templateId: "d-a1b2c3d4e5f6", // opaque ID from SendGrid UI
dynamicTemplateData: {
name, // {{name}} in Handlebars template
plan, // {{plan}} in Handlebars template
login_url: `https://yourapp.com/login`,
},
});
}The problem: that template ID is a pointer to HTML you can't see in your codebase. If someone changes the template in SendGrid's web editor, your code has no idea. No type checking, no version control, no way to test locally.
Resend + React Email: components with typed props
import { Resend } from "resend";
import { WelcomeEmail } from "@/emails/welcome";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail(
to: string,
name: string,
plan: "starter" | "pro" | "enterprise"
) {
await resend.emails.send({
to,
from: "hello@yourapp.com",
subject: `Welcome to YourApp, ${name}`,
react: <WelcomeEmail name={name} plan={plan} />,
});
}The template is a React component in your repo. TypeScript catches typos at build time. You can preview it locally with email dev. It lives in version control next to the code that sends it.
plan parameter: in SendGrid, it is an untyped string that could be anything. In React Email, it is a union type that your IDE autocompletes and your compiler enforces.The template rebuild trap
This is where migrations go sideways. You have 12 SendGrid templates that work fine. Now every single one needs to become a React Email component. The instinct is to start from scratch and hand-write each one.
// The DIY approach: every template is a 200+ line fight
// with table layouts and email client quirks
import {
Html, Head, Preview, Body, Container,
Section, Row, Column, Img, Text, Button, Hr
} from "@react-email/components";
interface WelcomeEmailProps {
name: string;
plan: string;
}
export function WelcomeEmail({ name, plan }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to YourApp, {name}</Preview>
<Body style={{ backgroundColor: "#f6f9fc", fontFamily: "..." }}>
<Container style={{ /* 30+ inline style properties */ }}>
{/* Header with logo */}
<Section style={{ padding: "32px 0", textAlign: "center" }}>
<Img
src="https://yourapp.com/logo.png"
width={120}
height={40}
alt="YourApp"
/>
</Section>
{/* Hero section - need tables for Outlook */}
<Section style={{ backgroundColor: "#ffffff", borderRadius: "8px" }}>
<Text style={{ fontSize: "24px", fontWeight: "bold" }}>
Welcome aboard, {name}
</Text>
<Text style={{ color: "#64748b", lineHeight: "1.6" }}>
You are on the {plan} plan. Here is what to do next...
</Text>
{/* ... 150 more lines of inline styles,
Outlook conditional comments,
mobile responsive hacks ... */}
</Section>
</Container>
</Body>
</Html>
);
}That is one template. You have 12 more: email verification, password reset, trial ending, payment receipt, failed payment, invoice, subscription renewal, cancellation confirmation. Each one needs the same boilerplate and the same Outlook workarounds. It adds up fast.
This is usually the point where someone on the team asks: “Do we really need to build all of these ourselves?”
DNS cutover: running both providers in parallel
You don't need to do a hard cutover. Both SendGrid and Resend can coexist during the transition. The key is DNS record ordering.
# Keep existing SendGrid records
TXT _dmarc.yourapp.com "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourapp.com"
TXT yourapp.com "v=spf1 include:sendgrid.net include:resend.dev ~all"
CNAME s1._domainkey.yourapp.com s1.domainkey.u12345.wl.sendgrid.net
# Add Resend DKIM records alongside
CNAME resend._domainkey.yourapp.com resend._domainkey.resend.devThe SPF record includes both sendgrid.net and resend.dev. This lets you send from either provider while you migrate individual email flows one at a time.
include:sendgrid.netfrom your SPF record. Keeping unused SPF includes doesn't hurt deliverability, but it adds unnecessary DNS lookups. SPF has a 10-lookup limit - every include counts.Migrate flow by flow, not all at once
The safest migration strategy: move one email flow at a time, starting with the lowest-risk template.
| Priority | Email Flow | Risk Level | Why This Order |
|---|---|---|---|
| 1 | Welcome email | Low | Non-critical, easy to test, high volume for quick validation |
| 2 | Email verification | Low | Simple template, clear success/failure signal |
| 3 | Password reset | Medium | Security-critical but low volume, test thoroughly |
| 4 | Trial ending / lifecycle | Medium | Revenue impact, but time-delayed so you can monitor |
| 5 | Payment receipt / invoice | High | Compliance requirements, financial data, migrate last |
| 6 | Failed payment recovery | High | Direct revenue impact, ensure deliverability is stable first |
Recommended migration order: low risk first, revenue-critical last
Each flow you migrate is a chance to verify Resend's deliverability with your domain. By the time you reach the high-risk payment flows, you'll have weeks of data confirming everything works.
What teams actually save
The numbers on a typical SaaS migration with 10-15 email templates tell the story pretty clearly.
| Item | DIY from Scratch | With Pre-Built Templates |
|---|---|---|
| Template development | 40-80 hours (3-5 hrs per template x 10-15) | 4-8 hours (customizing colors, copy, logo) |
| Cross-client testing | 15-20 hours (Outlook, Gmail, Apple Mail, Yahoo) | 0 hours (pre-tested across 90+ clients) |
| Mobile responsiveness | 10-15 hours (breakpoints, font scaling, touch targets) | 0 hours (built in) |
| Dark mode support | 8-12 hours (per-client media queries, fallbacks) | 0 hours (built in) |
| Total engineering time | 73-127 hours | 4-8 hours |
| At $150/hr senior dev rate | $10,950-$19,050 | $600-$1,200 + template cost |
Time comparison: building React Email templates from scratch vs. using a pre-built library
The template cost pays for itself before you finish customizing the second one. But the bigger win is the cross-client testing you skip entirely. Every template has already been verified across Outlook (2016-2024), Gmail, Apple Mail, Yahoo Mail, and Outlook.com. That testing alone would take a week if you did it yourself.
Common gotchas during the switch
1. Suppression list migration
SendGrid maintains a suppression list of addresses that bounced or unsubscribed. Export this list before you cancel your SendGrid account. Sending to suppressed addresses from Resend will damage your new domain reputation.
# Export bounces and unsubscribes from SendGrid API
curl -X GET "https://api.sendgrid.com/v3/suppression/bounces" \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json" | jq '.[] | .email' > bounces.txt
curl -X GET "https://api.sendgrid.com/v3/suppression/unsubscribes" \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json" | jq '.[] | .email' > unsubscribes.txt2. Webhook event format differences
SendGrid and Resend use different webhook payload formats. If you process delivery events, bounces, or opens, your webhook handler needs updating.
// SendGrid webhook event
interface SendGridEvent {
event: "delivered" | "bounce" | "open";
email: string;
sg_message_id: string;
timestamp: number;
}
// Resend webhook event
interface ResendEvent {
type: "email.delivered" | "email.bounced" | "email.opened";
data: {
email_id: string;
to: string[];
created_at: string;
};
}
// Your webhook handler needs to handle the new format
export async function POST(req: Request) {
const body = await req.json();
// Resend uses dot-notation event types
switch (body.type) {
case "email.delivered":
await handleDelivered(body.data);
break;
case "email.bounced":
await handleBounce(body.data);
break;
}
return Response.json({ received: true });
}3. Rate limits are different
SendGrid's free tier allowed 100 emails/day. Resend's free tier allows 3,000 emails/month (100/day). The paid tiers are where the real difference shows: Resend's $20/month plan includes 5,000 emails, while SendGrid's $19.95 Essentials plan includes 100,000 but charges overage fees that add up fast.
After the migration: what you gain
The migration itself is a one-time cost. The day-to-day difference in how you work with email templates is what makes people glad they switched.
| Workflow | SendGrid | Resend + React Email |
|---|---|---|
| Edit a template | Log into web UI, find template, edit in browser, save, hope it works | Edit .tsx file, see changes in local preview, commit to git |
| Review template changes | No diff view, no PR review, no audit trail | Standard PR review with full diff, visual preview in CI |
| Test a template | Send test email from dashboard, check your inbox manually | Run local dev server, hot reload, render tests in CI |
| Share a template across teams | Duplicate in SendGrid UI, hope both stay in sync | Import shared component, single source of truth |
| Debug rendering issues | View raw HTML in SendGrid, guess which client breaks | React Email CSS checker flags unsupported properties per client |
Developer experience comparison after migration
- Export your SendGrid suppression lists (bounces + unsubscribes) before cancelling
- Add Resend DNS records alongside SendGrid - both providers can coexist during migration
- Replace
@sendgrid/mailAPI calls with the Resend SDK - the surface area is smaller and fully typed - Migrate templates flow by flow: welcome first, payment flows last
- Use production-ready React Email templates instead of rebuilding from scratch - saves 60-120 hours of engineering time
- Update webhook handlers for Resend's event format (dot-notation types, different payload structure)
- Remove SendGrid SPF include from DNS once all flows are migrated
- Monitor bounce rates in Resend dashboard for the first 48 hours after cutover