TL;DR: The thing that broke my trust in email providers wasn't an outage or a billing surprise — it was silence. One provider I was running for a SaaS product was quietly dropping roughly 3% of sends.
📖 Reading time: ~35 min
What's in this article
- I Needed a Reliable Email Provider and Got Burned Twice First
- Quick Setup Reality Check — How Long Does Each Take to Go Live?
- Head-to-Head Comparison Table
- The Moment Each Provider Won (and Lost) for Me
- When to Pick What — Match the Tool to Your Actual Situation
- Gotchas That Will Cost You Time
- My Current Setup and What I'd Change
I Needed a Reliable Email Provider and Got Burned Twice First
The thing that broke my trust in email providers wasn't an outage or a billing surprise — it was silence. One provider I was running for a SaaS product was quietly dropping roughly 3% of sends. No bounce. No error in the API response. Successful 200s all the way down. I only caught it because a user complained they never got their password reset email, I dug into the logs, and the math didn't add up between sends and actual deliveries. Three percent sounds small until you realize that's 1 in 33 users potentially unable to reset their password or confirm their account on any given day.
That experience is the entire reason this comparison exists. I went deep on three providers that each win a different argument: AWS SES if you're already on AWS and want the cheapest possible per-email cost at scale, Postmark if deliverability reputation is non-negotiable and you want a provider that's been obsessive about inbox placement for years, and Resend if you want the best developer experience in 2026 — clean SDK, React Email integration, sane dashboard. None of them is the universal right answer, and I'm going to show you exactly where each one earns its recommendation and where it falls apart.
One hard scope boundary before we go further: this is about transactional email. Password resets. Invoice receipts. Notification emails. Account confirmations. The kind of email where a failed send has a direct user impact and you need delivery receipts you can actually trust. Bulk marketing email — newsletters, drip campaigns, promotional blasts — operates under completely different deliverability rules, has different legal requirements, and frankly needs different infrastructure. If you're sending 500K promotional emails a month, you want Klaviyo or Mailchimp or SendGrid's marketing tier, not any of the three tools I'm covering here. For a complete list of workflow tools we actually use, check out our guide on Productivity Workflows.
What I'll cover: real pricing with actual numbers from each provider's current pricing page, the setup friction involved in getting production-ready (SES will humble you here), deliverability differences that aren't just marketing copy, the gotchas I hit after a few weeks of real usage on each, and the specific situations where I'd pick one over the other. I've run all three in production across different projects — a small B2B SaaS on Resend, a higher-volume app on SES, and a client project where Postmark was the non-negotiable choice after the 3% drop incident. The comparisons below come from that, not from reading docs in an afternoon.
Quick Setup Reality Check — How Long Does Each Take to Go Live?
The gap between "account created" and "email delivered to a real inbox" is where you lose hours you didn't budget for. I've gone through all three setups in the past year, and the time-to-first-real-email varies wildly — not because of complexity, but because of decisions made by the product teams about trust and onboarding.
AWS SES: Expect 30–90 Minutes, Plus a Trap That Will Bite You
SES domain verification is straightforward if you're already in Route53. You add three CNAME records for DKIM, one TXT for domain verification, and SES validates them within 15–30 minutes. Outside Route53, you're doing manual DNS — still fine, but the propagation wait is unpredictable. The DKIM setup Amazon generates looks like this in your zone:
# SES gives you three of these CNAME records
abc123._domainkey.yourdomain.com CNAME abc123.dkim.amazonses.com
def456._domainkey.yourdomain.com CNAME def456.dkim.amazonses.com
ghi789._domainkey.yourdomain.com CNAME ghi789.dkim.amazonses.com
# Plus the TXT verification record
_amazonses.yourdomain.com TXT "v=spf1 include:amazonses.com ~all"
Here's the trap that trips everyone: you are in sandbox mode by default, and sandbox mode only delivers to email addresses you've explicitly verified. Send to any unverified address and you get a 200 OK from the API with zero delivery. No error. No bounce. Nothing in your logs. The email just disappears. I've watched this confuse junior devs for two hours who were convinced their code was broken. To exit sandbox mode, you file a support request through the console — AWS reviews it, usually within 24 hours. You can also force-enable sending via CLI, but that doesn't skip the sandbox restriction:
# This enables sending but does NOT move you out of sandbox
aws ses put-account-sending-attributes --sending-enabled
# To actually exit sandbox, you go through:
# SES Console → Account Dashboard → Request Production Access
# There's no CLI shortcut around the manual review
To catch the sandbox problem early, add an explicit check in your test suite. Query your account's sending quota — if Max24HourSend is 200, you're still in sandbox:
aws ses get-send-quota
# Sandbox output:
# {
# "Max24HourSend": 200.0,
# "MaxSendRate": 1.0,
# "SentLast24Hours": 0.0
# }
# Production accounts have Max24HourSend in the tens of thousands
Postmark: Slower to Start, but They Actually Look at Your Account
Postmark manually reviews new accounts before you can send to external addresses. This takes anywhere from a few hours to one business day. Annoying? Yes. But it's also why Postmark's deliverability reputation is genuinely strong — they don't let spammers self-service their way in. Once approved, DKIM and Return-Path setup is cleaner than SES. You add their DKIM record and one CNAME for the Return-Path domain, and the dashboard shows green within minutes. From that point, your first real API call takes under 10 minutes:
curl "https://api.postmarkapp.com/email" \
-X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Postmark-Server-Token: your-server-token" \
-d '{
"From": "you@yourdomain.com",
"To": "recipient@example.com",
"Subject": "Test from Postmark",
"TextBody": "It works."
}'
# Returns 200 with MessageID and submission timestamp immediately
The honest tradeoff: that human review gate is annoying if you're prototyping at 11pm and need to demo something in the morning. Plan for it. Postmark is not the right choice if you need zero-to-sending in 30 minutes today.
Resend: Five Minutes, For Real — Here's What That Costs You
Resend is genuinely the fastest path to a working email integration I've seen. Install the SDK, grab an API key from the dashboard, and you're sending:
npm install resend
# then in your code:
import { Resend } from 'resend';
const resend = new Resend('re_yourApiKey');
await resend.emails.send({
from: 'you@yourdomain.com',
to: 'recipient@example.com',
subject: 'Hello',
html: 'It works.',
});
That's the entire integration. Domain verification in Resend is also faster than SES because the dashboard UX is better — you get one TXT record and one set of DKIM CNAMEs, clearly labeled, and verification happens within a few minutes. The speed tradeoff is real though: Resend's free tier caps at 3,000 emails/month and 100/day. Their paid plans start at $20/month for 50,000 emails. More importantly, Resend is newer than the other two — their status history is shorter, their edge case docs are thinner, and if something breaks in an unusual way, the community troubleshooting pool is smaller. For a side project or a startup's transactional email, that's fine. For a system sending millions of critical password resets, I'd want more track record behind it.
- SES: 30–90 min for DNS + potentially 24h to exit sandbox. Cheapest at scale ($0.10/1,000 emails), but the sandbox silent-failure gotcha will waste your time if you're not watching for it.
- Postmark: Human review adds a day, but once you're in, setup is clean and deliverability is the strongest of the three for transactional mail. Starts at $15/month for 10,000 emails.
- Resend: Fastest onboarding by a wide margin. Great DX, honest pricing, but lean on battle-tested references before committing it to anything mission-critical at scale.
AWS SES: Dirt Cheap Until It Isn't
The suppression list behavior is the thing that will burn you and SES's documentation buries it. When an email hard-bounces, SES automatically adds that address to your account-level suppression list — silently, with no notification, no webhook, nothing. Future sends to that address get dropped with a MessageRejected error that's easy to miss if you're not scraping your send logs carefully. I've seen teams spend days debugging "deliverability issues" that were just suppression list accumulation. Audit it now:
aws sesv2 list-suppressed-destinations \
--filter REASONS=BOUNCE \
--region us-east-1 \
--output json | jq '.SuppressedDestinationSummaries[] | {email: .EmailAddress, reason: .Reason, date: .LastUpdateTime}'
If you want to remove a legitimate address (say, a bounce that was a temporary server error), you do it with aws sesv2 delete-suppressed-destination --email-address user@example.com. There's no bulk removal in the CLI — you script a loop. This is a real ops cost that doesn't show up in the pricing page.
For the actual send code, skip SMTP entirely. The SMTP relay adds latency and you lose observability. The SESv2 API via boto3 is what I run in 2026:
import boto3
from botocore.exceptions import ClientError
client = boto3.client('sesv2', region_name='us-east-1')
try:
response = client.send_email(
FromEmailAddress='noreply@yourdomain.com',
Destination={'ToAddresses': ['user@example.com']},
Content={
'Simple': {
'Subject': {'Data': 'Your invoice is ready', 'Charset': 'UTF-8'},
'Body': {
'Html': {'Data': 'Here is your invoice.', 'Charset': 'UTF-8'},
'Text': {'Data': 'Here is your invoice.', 'Charset': 'UTF-8'}
}
}
},
# ConfigurationSetName is critical — this is how you route events to SNS/CloudWatch
ConfigurationSetName='transactional-prod'
)
# response['MessageId'] looks like: '010f018e1234abcd-abc12345-...-000000'
# That MessageId is what you store for bounce correlation
print(response['MessageId'])
except ClientError as e:
# ErrorCode you actually care about: 'MessageRejected', 'AccountSendingPausedException'
print(e.response['Error']['Code'])
The ConfigurationSetName parameter is not optional if you want any feedback loop. Without a configuration set wired to an SNS topic, you're flying blind on bounces and complaints. Setting that up takes about 30 minutes the first time — create the SNS topic, subscribe an SQS queue to it, configure the event destination in SES, write the consumer. That consumer is your permanent responsibility now.
The Reputation Dashboard in the SES console shows bounce rate and complaint rate in near-real-time. AWS's published thresholds are: bounce rate above 5% puts you under review, above 10% and your sending can get paused. Complaint rate (spam reports) above 0.1% triggers a review, and above 0.5% is genuinely dangerous to your account. These aren't soft warnings — I know a startup that had their account suspended on a Saturday because a marketing blast hit a cold list. There's no automatic recovery; you open a support ticket and wait.
The cost math is real though. SES charges $0.10 per 1,000 emails sent, plus $0.12 per GB of attachments. If you're sending from an EC2 instance or Lambda inside AWS, the first 62,000 emails per month are free. At 500,000 emails a month you're paying roughly $50 in SES fees — compare that to Postmark at that volume which runs closer to $400/month. The delta is real money. But be honest with yourself about the ops overhead: configuration sets, bounce queues, suppression list management, reputation monitoring, and the inevitable 2am page when your complaint rate spikes. If your team doesn't have someone who owns email infrastructure, that $350/month difference evaporates fast in engineering time.
Postmark: The One You Reach for When Deliverability Has to Be Right
The thing that caught me off guard about Postmark wasn't the deliverability — it was the Message Streams architecture. Every Postmark server has two types: transactional streams (for password resets, receipts, alerts) and broadcast streams (for newsletters, marketing blasts). These aren't just organizational labels. Postmark physically routes them through different IP pools with different sending reputations. If your marketing list goes cold and someone marks you as spam, that doesn't bleed into your transactional reputation. For most ESPs, your entire account shares a reputation. With Postmark, a bad broadcast campaign can't tank your password reset deliverability. That alone justifies the account structure for any product doing both types of email.
The API is refreshingly clean. Here's an actual send with verbose output so you can see what happens at each status:
# Successful transactional send
curl -s -X POST https://api.postmarkapp.com/email \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'X-Postmark-Server-Token: YOUR_SERVER_TOKEN' \
-d '{
"From": "noreply@yourdomain.com",
"To": "user@example.com",
"Subject": "Your receipt",
"TextBody": "Thanks for your order.",
"HtmlBody": "Thanks for your order.",
"MessageStream": "outbound"
}'
# 200 response looks like:
{
"To": "user@example.com",
"SubmittedAt": "2026-01-15T10:32:00.0000000+00:00",
"MessageID": "b7bc2f4a-e38e-4336-af2d-bb2a8e0014b7",
"ErrorCode": 0,
"Message": "OK"
}
# 422 (validation failure) looks like:
{
"ErrorCode": 300,
"Message": "Invalid 'To' address: 'notanemail'."
}
# ErrorCode 406 means the address is on their suppression list —
# do NOT retry these, mark them inactive in your own DB immediately
The 422 error codes are actually meaningful — Postmark has a full list. ErrorCode 406 (inactive recipient) is the one you need to handle explicitly in code. If you keep sending to a 406 address, Postmark will eventually hold your messages for review. Wire up your error handler to distinguish between retry-safe errors (rate limits, 5xx transient) and permanent failures (300-series) from day one.
Bounce webhooks are where Postmark gets serious. When a bounce happens, Postmark POSTs to your endpoint with a payload you actually want to read:
{
"RecordType": "Bounce",
"Type": "HardBounce",
"TypeCode": 1,
"MessageID": "b7bc2f4a-e38e-4336-af2d-bb2a8e0014b7",
"Email": "user@example.com",
"BouncedAt": "2026-01-15T10:32:45.0000000+00:00",
"Details": "smtp;550 5.1.1 The email account does not exist",
"CanActivate": false
}
HardBounce with CanActivate: false — remove that email from your list permanently and never send to it again. SoftBounce (TypeCode 2) means the mailbox was full or temporarily unavailable — you can retry after 24-48 hours, but if you get three soft bounces in a row for the same address, treat it as a hard bounce in your own logic. Postmark doesn't make that decision for you. The complaint webhook payload is identical in structure but RecordType: "SpamComplaint" — same treatment as hard bounce, immediate suppression, no exceptions. Wire this in Express or FastAPI as a plain POST endpoint, verify the X-Postmark-Signature header (they use HMAC-SHA256), and store every event in your own database before you do anything else with it.
Honest take on pricing: Postmark charges around $1.50 per 1,000 emails on the starter plan, with a $15/month minimum covering 10,000 emails. At 500 emails/month you're paying $15 for something you could get cheaper elsewhere — that's the painful part. The math shifts once you're consistently sending 50K+ emails monthly and your product has real users who depend on receiving those emails. Password reset failing at 2am because your shared IP got temp-blocked on AWS SES is a support ticket, a lost user, and sometimes a chargeback. At that scale the deliverability premium pays for itself in support hours you don't spend. Below 10K emails/month, consider whether you actually need Postmark or whether a well-configured Resend account will get you 90% of the way there for less.
On templates: Postmark has a template API where you store templates on their servers and send like this — "TemplateId": 1234567, "TemplateModel": {"name": "Alice", "amount": "$49.00"}. The upside is designers can iterate without touching your codebase and you get a visual preview tool. The downside is your templates live in a third-party SaaS, which means version control is manual, rollbacks are painful, and a Postmark outage takes your template rendering with it. My preference is to keep templates in your repo as .mjml or .hbs files, compile them at deploy time, and send pre-rendered HTML via the API. You own the rendering, you can test it in CI, and you're not locked to one provider's template syntax. Use Postmark's template API only if you have a non-technical team that needs to edit email copy without a PR cycle.
Resend: Built for Developers, But How Far Does That Get You?
The React Email integration is the thing that actually made me stop and pay attention. You scaffold a template with npx create-email@latest, get a hot-reloading preview server at localhost:3000, and the whole thing composes like a React app. No more editing HTML strings in a config file and praying the Gmail client renders your inline styles. When you're ready to send, you pass the rendered component directly to resend.emails.send() — no intermediate stringify step, no template IDs to manage in a dashboard:
import { Resend } from 'resend';
import { WelcomeEmail } from './emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
// render() from @react-email/components handles the HTML conversion
const { data, error } = await resend.emails.send({
from: 'onboarding@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome aboard',
react: ,
});
// data is typed as CreateEmailResponse | null
// { id: string } — that's basically the whole shape
if (error) {
// error is ResendErrorResponse | null
// fields: name, message, statusCode
console.error(`Send failed: ${error.message} (${error.statusCode})`);
}
The TypeScript types are... functional, but sparse. CreateEmailResponse gives you back an id: string and that's about it on success. The error shape is consistent — name, message, statusCode — which is better than a lot of SDKs I've dealt with. What bit me was that error being null doesn't guarantee the email actually queued. You need to set up webhooks and track the email.sent event yourself if you care about downstream state. The SDK doesn't throw on failure by default — it returns the tuple — which is a fine design choice, but you have to remember to actually check error every time or you're silently eating failures.
The free tier at time of writing is 3,000 emails/month and 100/day. That daily cap is the real constraint — it's not about monthly volume, it's that you can't even do a small batch send without bumping into it. Paid plans start at $20/month for 50,000 emails. Before you commit, go check resend.com/pricing directly because they've adjusted limits more than once. The thing worth budgeting for isn't the price — it's the migration conversation if you outgrow the tier and your templates are now deeply coupled to React Email and the Resend-specific send API.
The deliverability question is the honest conversation nobody has upfront. Resend launched in 2023. That's not a knock — it's a fact that affects IP reputation. Shared sending pools accumulate reputation over years of sending, and Resend's infrastructure simply hasn't had the same runway as Postmark (2009) or SES. For transactional email to personal inboxes — password resets, receipts, notifications — I haven't seen meaningful deliverability differences in my own testing. Where I'd be cautious is high-stakes bulk sends to older corporate domains with aggressive spam filters. If you're a B2C product sending to Gmail/Outlook personal addresses, you're probably fine. If you're hitting procurement@ and security@ addresses at Fortune 500 companies, I'd want more historical data before going all-in.
Webhook support works, but it's noticeably less mature than Postmark's implementation. Resend gives you the core events — email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked. You configure the endpoint in the dashboard and verify with a shared secret. What Postmark has that Resend doesn't: per-message-stream webhook configurations, more granular retry logic documentation, and a webhook event log you can actually query. Resend's dashboard event log exists but it's paginated and not easily filterable for debugging a specific send. If your architecture depends on tight bounce handling loops — suppression list management, retry policies, CRM sync on opens — you'll spend more time rolling your own logic with Resend than you would with Postmark, where that operational surface is more thought-through.
Head-to-Head Comparison Table
I've burned time on all three of these in production contexts, and the differences that actually matter don't show up in the marketing pages. Here's what the comparison looks like when you strip out the fluff:
Dimension
AWS SES
Postmark
Resend
Setup time to production
30–60 min if you already know IAM, SESv2, and aren't starting from a new account in sandbox mode
10–20 min after manual account approval (expect 1–2 business days)
Under 10 min — API key, DNS records, done
Free tier
62,000 emails/month free only when sending from EC2 or Lambda inside AWS. Otherwise pay-per-send from day one.
Check postmarkapp.com/pricing — they do offer a small monthly credit for testing
Check resend.com/pricing — free tier exists with monthly send cap
Pricing model
Per-email. Extremely cheap at scale. No monthly minimums outside of dedicated IPs (~$24.95/IP/month).
Per-email with volume tiers. No gotcha minimums, but dedicated IPs cost extra.
Per-email with volume tiers. Pricing is simple — no confusing add-ons.
Deliverability tools
CloudWatch metrics, SESv2 suppression list, Virtual Deliverability Manager (VDM) — powerful but you have to wire it up yourself
Message Streams separate transactional from broadcast traffic at the infrastructure level. Best-in-class reputation management baked in.
Standard open/click/bounce tracking. React Email integration is the differentiator, not advanced deliverability tooling.
SDK quality
AWS SDK v3 for JS/Python/Go/etc. Functional but verbose — you're calling SESv2Client with a SendEmailCommand payload. Not ergonomic.
Official client libraries are solid. The Node SDK is clean and well-maintained.
First-class TypeScript SDK. resend.emails.send() takes 30 seconds to write. Best DX of the three.
Webhook maturity
SNS-based event notifications. Works, but the setup is: SES → SNS topic → SQS or HTTP endpoint. Three AWS services to configure for what competitors do in one step.
Bounce, complaint, delivery, open, click webhooks — all mature, well-documented, and reliable. These have been stable for years.
Webhook support is growing. Delivery events work. Feature velocity is high so gaps are closing fast, but it's not as battle-tested as Postmark's.
Suppression management
Account-level suppression list you must manage. Bounces auto-add addresses. You're responsible for honoring unsubscribes beyond this.
Handles bounce and complaint suppression automatically per Message Stream. Much less manual overhead.
Suppression list handled automatically. Unsubscribe link management is straightforward.
Best for
High-volume AWS-native infrastructure, teams comfortable with IAM, shops sending millions of emails where per-email cost is the primary concern
Teams where transactional deliverability is non-negotiable — password resets, receipts, anything where landing in spam is a real business problem
JS/TS teams who want to ship email in an afternoon without reading 40 pages of docs. Startups moving fast.
The biggest gotcha with SES that nobody warns you about: new accounts start in sandbox mode, which means you can only send to verified addresses until you submit a production access request. AWS reviews these manually. I've seen it take 24 hours, I've seen it take a week. If you're trying to ship next Tuesday, that's a real constraint. Postmark also does manual approval but their process is faster and more transparent about what they're looking for.
Resend's resend.emails.send() API is genuinely the fastest path from zero to working email I've found. Here's what a real call looks like:
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
// Errors surface as typed responses, not thrown exceptions — good design choice
const { data, error } = await resend.emails.send({
from: 'notifications@yourdomain.com',
to: 'user@example.com',
subject: 'Your order shipped',
react: OrderShippedEmail({ orderNumber: '1234', trackingUrl: 'https://...' }),
});
if (error) {
console.error('Send failed:', error.message);
}
Compare that to the SESv2 equivalent where you're building a SendEmailCommand with a nested Content → Simple → Subject → Data object tree, and you feel the ergonomic gap immediately. SES isn't bad — it's just clearly designed by infra engineers for infra engineers, not for developer experience.
One hard note on pricing: I'm deliberately not hardcoding numbers in this comparison because email provider pricing shifts regularly and stale numbers are worse than no numbers. Check aws.amazon.com/ses/pricing, postmarkapp.com/pricing, and resend.com/pricing directly before making a decision. The structural difference — SES is cheapest at scale, Postmark and Resend charge more per email but include more infrastructure around it — has been stable even as the specific numbers change.
The Moment Each Provider Won (and Lost) for Me
SES made sense the moment I ran the math on a notification pipeline pushing several million emails a month. At $0.10 per 1,000 emails, the gap versus Postmark's per-email pricing isn't abstract — it's a line item that shows up in budget reviews. We had a backend engineer who'd already wired up SNS topics for bounce and complaint handling, so the operational overhead wasn't scary. When you have that setup dialed in, SES is genuinely the right call. Nobody is going to convince me to pay 10x more for delivery infrastructure when I have the tooling to manage it myself.
But SES has a specific failure mode that burned us hard. The global suppression list — the one that's account-level and not domain-level — silently swallowed password reset emails for roughly 15 users after a bounce event triggered an automatic suppression entry. These weren't spam complaints. One user had a full inbox, bounced once, got suppressed, then when they legitimately needed a password reset they just... never got the email. No error on our end. SES accepted the send, returned a MessageId, and moved on with its life. Those users couldn't get back into their accounts without a manual support intervention, and half of them had already churned before we caught it. If you're using SES for anything transactional, build a pre-send suppression check into your send path or you will have this exact incident:
# Check before sending — don't rely on SES to tell you after the fact
aws sesv2 get-suppressed-destination \
--email-address user@example.com
# If it returns a suppression record, decide whether to remove it
aws sesv2 delete-suppressed-destination \
--email-address user@example.com
Postmark earned its reputation on a client project where inbox placement wasn't negotiable. B2B SaaS, $400/month average contract value, password resets and invoice emails were the product's backbone. I've seen Postmark's transactional stream consistently hit primary inbox on Gmail and Outlook where other providers were landing in Promotions or, worse, Spam. The reason isn't magic — it's that Postmark's IPs are dedicated to transactional mail and they enforce that aggressively. They reject bulk senders. That shared reputation pool actually protects you. For that client, I never had to explain to a customer why their invoice was in spam. That's worth paying for.
Where Postmark lost was a side project I was running with maybe 200 users. The minimum commitment and per-email pricing felt misaligned with the risk profile — I was paying for deliverability guarantees on a product that hadn't yet proven people wanted it. Postmark charges around $15/month minimum for their starter tier. That's not a lot of money, but psychologically, paying for infrastructure quality on an unvalidated product felt backwards. I switched to Resend for that project and used the savings to buy ads instead.
Resend genuinely surprised me on a greenfield Next.js 14 project. The team was already deep in React, and the React Email + Resend combo meant our designer could open a component file, tweak the layout, preview it in the browser like a normal React component, and the change was live without a backend deploy. That's a workflow shift that's hard to quantify but real to live through. The integration is three lines:
import { Resend } from 'resend';
import { WelcomeEmail } from '@/emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'onboarding@yourdomain.com',
to: user.email,
subject: 'Welcome',
react: ,
});
The place Resend fell short was anything requiring mature suppression management. We needed to build our own suppression audit logic, reconcile bounces back to user records, and handle complaint webhooks ourselves in a way that with Postmark or SES would have been more documented and battle-tested. Resend's webhook events are there, but the ecosystem of examples for edge cases — like re-engagement flows or suppression list exports — is thin compared to providers that have been doing this for a decade. We shipped it, but I'd estimate we spent an extra two weeks building plumbing that other providers give you out of the box.
When to Pick What — Match the Tool to Your Actual Situation
The trap most teams fall into is picking an email provider based on a benchmark post from 2022 or because it's what the boilerplate used. The actual decision depends on four things: your stack, your volume, your tolerance for ops work, and what happens when an email doesn't arrive. Those four variables point you in completely different directions.
Pick SES When You're Optimizing for Cost at Volume — and Can Absorb the Ops Overhead
SES pricing is genuinely hard to beat: $0.10 per 1,000 emails when sending from EC2 or Lambda, with the first 62,000 free per month if you're on EC2. At 500K emails/month, the cost difference between SES and Postmark is not trivial — we're talking hundreds of dollars monthly. But the bill only makes sense if you have someone who can own the SESv2 migration (the original SES API is being de-emphasized), suppression list management, bounce and complaint handling via SNS webhooks, and dedicated IP warmup if you go that route. If you're already running infrastructure in us-east-1 or eu-west-1 and have a DevOps engineer who touches AWS daily anyway, the marginal cost of owning SES properly is low. If that person doesn't exist, the savings evaporate in support tickets and deliverability firefighting.
# SESv2 send with suppression check — this is the minimum viable setup
aws sesv2 send-email \
--from-email-address "noreply@yourdomain.com" \
--destination '{"ToAddresses":["user@example.com"]}' \
--content '{"Simple":{"Subject":{"Data":"Your order shipped"},"Body":{"Text":{"Data":"..."}}}}' \
--configuration-set-name "production-config-set"
# ConfigurationSet is NOT optional — it's how you wire up SNS bounce/complaint topics
# Skipping it means you're flying blind on deliverability signals
Pick Postmark When a Missing Email Is a User Support Ticket — or Worse, a Churned User
Postmark's pitch is simple: they only do transactional email, their dedicated IP pools are pre-warmed and separated by message stream type, and their average delivery time to inbox hovers around 3–5 seconds. I've watched teams move password reset flows from SES to Postmark and cut "I never got my reset link" support tickets by a significant margin. The price is real — $15/month for 10K emails, scaling up — but if you've had an incident where a payment confirmation didn't arrive and the user did a chargeback, you already know that $15 is noise. Postmark's message streams (Transactional vs Broadcast) also enforce a clean IP reputation separation, which matters if you're sending any marketing email from the same account.
Pick Resend If You're Shipping Fast on a JS/TS Stack and Your Team Owns the Templates
Resend made a smart bet: React Email as the templating layer, a clean REST API, and an SDK that takes about 10 minutes to wire up. If your team is already in TypeScript all day and wants to write email templates as React components with real props and type safety, Resend fits without friction. The free tier is 3,000 emails/month and 100/day — fine for a product in early growth, but you'll hit the daily limit faster than you expect during a launch or a triggered campaign. Pricing scales to $20/month for 50K emails, which is competitive. The thing that caught me off guard was the domain verification requiring DNS propagation — budget 30 minutes and verify early, not the night before launch.
// Resend with React Email — this is what actually ships fast
import { Resend } from 'resend';
import { WelcomeEmail } from './emails/WelcomeEmail'; // React component
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'Acme ',
to: user.email,
subject: 'Welcome to Acme',
react: WelcomeEmail({ name: user.name, confirmUrl: token }),
// tags let you filter analytics per email type — use them from day one
tags: [{ name: 'category', value: 'onboarding' }],
});
The Hybrid Setup Nobody Talks About Publicly
Running SES for high-volume notification email (weekly digests, activity summaries, bulk alerts) and Postmark for critical transactional (password reset, payment confirmation, account deletion warnings) is operationally messier but financially rational past a certain scale. The complexity is real: two SDKs, two sets of webhooks, two deliverability dashboards to monitor. But if you're sending 1M notification emails a month, doing that on Postmark costs roughly $400+. On SES it's $100. The $300 monthly delta funds a junior engineer's tooling budget. The practical implementation is just routing logic in your email service layer — a single function that checks message type and dispatches to the right client. I've seen this pattern at three different companies and it works as long as exactly one person owns the abstraction layer and doesn't let it rot.
// email-router.ts — the abstraction that makes the hybrid sane
type EmailPriority = 'critical' | 'notification';
const CRITICAL_TYPES = new Set(['password_reset', 'payment_confirmation', 'account_locked']);
export async function sendEmail(params: EmailParams) {
const priority: EmailPriority = CRITICAL_TYPES.has(params.type)
? 'critical'
: 'notification';
if (priority === 'critical') {
return postmarkClient.sendEmailWithTemplate(params); // Postmark
}
return sesClient.sendEmail(params); // SESv2
}
What None of These Are Designed For
Cold outreach — prospecting emails sent to people who've never interacted with your product — is a completely different category. Using SES, Postmark, or Resend for cold outreach is how you get your account suspended and your domain reputation wrecked. Tools like Instantly, Lemlist, or Apollo handle the compliance requirements, reply tracking, unsubscribe management, and the deliverability gymnastics that cold volume demands. They use separate infrastructure specifically to keep that traffic isolated from your transactional sending reputation. The moment you mix cold outreach into a transactional sending domain, you're borrowing against reputation you spent months building. The tools covered here are for email your users asked to receive — that's the only use case where the deliverability investments these providers have made actually pay off for you.
Gotchas That Will Cost You Time
The SESv1 vs SESv2 thing burned me more than I expected. If you're starting a new integration today, pretend SESv1 doesn't exist. The SDKs aren't just versioned differently — the mental model for things like configuration sets, sending identities, and suppression lists is fundamentally restructured. I made the mistake of copying a Stack Overflow example from 2021 that used ses.sendEmail() from @aws-sdk/client-ses and spent two hours figuring out why my configuration set metrics weren't showing up. They were, just in a completely different place in the SESv2 console. Use @aws-sdk/client-sesv2 and SendEmailCommand from day one. Don't mix them.
# Wrong — old pattern, still works but will confuse you
aws ses send-email --from sender@example.com ...
# Right — use SESv2 from the CLI too
aws sesv2 send-email \
--from-email-address sender@example.com \
--destination '{"ToAddresses":["user@example.com"]}' \
--content '{"Simple":{"Subject":{"Data":"Test"},"Body":{"Text":{"Data":"Hello"}}}}'
The MAIL FROM domain setup on SES is the sneaky one. By default, your return path shows up as something like bounces.us-east-1.amazonses.com, which means the domain in your envelope sender doesn't match your sending domain. Spam filters notice this. It's not a hard block, but it's a yellow flag that chips away at your sender reputation over time. Setting up a custom MAIL FROM subdomain — something like mail.yourdomain.com — takes about 10 minutes in the console and requires two DNS records (an MX and a TXT), but most people skip it because SES never makes it feel mandatory. It is, if you care about inbox placement.
Postmark's message stream type is permanent. Full stop. If you create a stream and pick "Transactional", you can never convert it to "Broadcast" for newsletters. I discovered this after setting up a stream I thought would handle both onboarding flows and product announcements, assuming I could reconfigure it later. You can't. Delete and recreate. The reason Postmark does this is defensible — transactional and broadcast have different delivery pools and different compliance behavior — but the UI doesn't make it obvious during setup that this decision is irreversible. When you're creating a stream, slow down and think about whether this is triggered 1:1 email or bulk sends, then pick accordingly.
Resend's React Email preview server is genuinely useful during development — hot reload, component props, the whole thing. But "looks good in the preview" is not the same as "works in Outlook 2019" or "renders correctly in Gmail on Android". The preview renders in a modern Chromium environment. It has no idea what Apple Mail's dark mode quirks look like, how Outlook clips long preheaders, or whether your CSS resets survive Gmail's aggressive inline style stripping. I still run final templates through Litmus or Email on Acid before shipping anything to a list. That's not a knock on Resend specifically — every templating tool has this problem. But people new to React Email sometimes assume the preview is the source of truth. It's not.
All three providers will handle DKIM signing for your domain, but none of them can fix your DNS records for you. Here's the failure mode I've seen: someone sets up SES, adds the DKIM CNAME records SES generates, then forgets that their domain's SPF record is a leftover from a previous provider and now fails alignment. DMARC then fails because neither SPF nor DKIM align to the From: domain. The fix is boring but necessary — validate your full auth chain before you go to production.
# Check SPF, DKIM, and DMARC alignment from the command line
# Replace with your actual sending domain
dig TXT yourdomain.com | grep spf
dig TXT _dmarc.yourdomain.com
# Or use this to simulate what a receiving MTA sees
# after you send a test message — paste raw headers into:
# https://mxtoolbox.com/EmailHeaders.aspx
# Look for: dkim=pass, spf=pass, dmarc=pass — all three
The specific record pattern that catches people with SES: your SPF record needs to include include:amazonses.com, and if you've also got G Suite or Postmark or anything else in your history, you might be over the 10 DNS lookup limit for SPF. SPF has a hard cap of 10 include: lookups, and if you blow past it, the record fails to evaluate and you get a softfail. Tools like dmarcian's SPF surveyor will tell you your lookup count. Fix this before you wonder why your emails are landing in spam despite "everything looking correct".
My Current Setup and What I'd Change
I'm running Resend for a Next.js SaaS right now and it's been the right call at early stage. The React Email integration means I write templates in JSX, preview them in a browser, and the same component renders in the email. That alone saved me hours I would have burned wrestling with MJML or inlining styles by hand. The API is dead simple — one POST /emails call, sensible TypeScript types, done. But I'm already thinking about what changes when the user base grows past a few thousand accounts. Auth emails — password resets, magic links, account lockouts — those are the ones where a 30-second delay turns into a support ticket and a potential churn event. For those, I'll probably move to Postmark as volume picks up, specifically because their transactional delivery reputation is isolated from marketing sends in a way that Resend doesn't enforce as strictly yet.
The thing I wish someone had told me at the start: stop optimizing for cost at sub-10,000 emails a month. At that scale the dollar difference between providers is noise — you're talking $0 to maybe $20/month across all three. What actually matters is whether your password reset lands in the inbox vs. spam, whether the SDK has good TypeScript types, and whether the dashboard shows you per-email delivery status without making you dig through logs. I spent time evaluating SES at the start because "it's cheaper." It is. But configuring DKIM, SPF, and DMARC, then waiting through the sandbox lifting process, then wiring up SNS for bounce handling — that's easily a day of work. At 500 emails/month, that's an absurd trade-off. Save SES for when you're sending at volume and have the engineering bandwidth to treat email infrastructure as a proper project.
Vendor lock-in across these three is real but not fatal. The APIs are different enough that you can't just swap an env variable and call it done. Resend's SDK looks like this:
await resend.emails.send({
from: 'noreply@yourapp.com',
to: user.email,
subject: 'Reset your password',
react: <PasswordResetEmail token={token} />,
});
Postmark's client wants a TemplateAlias or raw HTML body, and SES wraps everything in a SendEmailCommand from @aws-sdk/client-ses. None of these are wildly complex, but if you've scattered resend.emails.send() calls across 12 different route handlers, migrating means touching all 12. The fix is boring but it works: one file, one interface, provider swapped in one place.
Here's the pattern I use — an email.service.ts that wraps the provider completely:
// email.service.ts
// Change the implementation here, nowhere else in the codebase
import { Resend } from 'resend';
const client = new Resend(process.env.RESEND_API_KEY);
interface SendEmailOptions {
to: string;
subject: string;
html: string;
replyTo?: string;
}
export async function sendEmail(opts: SendEmailOptions): Promise {
const { error } = await client.emails.send({
from: process.env.EMAIL_FROM!, // e.g. "App Name "
to: opts.to,
subject: opts.subject,
html: opts.html,
reply_to: opts.replyTo,
});
if (error) {
// log to your observability stack before throwing
throw new Error(`Email send failed: ${error.message}`);
}
}
Every route handler, every background job, every webhook processor calls sendEmail() and knows nothing about Resend. When I switch the auth emails to Postmark, I'll create a sendTransactionalEmail() that uses a separate Postmark client, point it at a dedicated message stream, and the rest of the app is untouched. You can make this more sophisticated with a factory pattern and a EMAIL_PROVIDER env var, but for most early-stage apps the simple version above buys you 90% of the benefit. The key thing is the boundary — your app code should be allergic to provider-specific types leaking out of that file.
Disclaimer: This article is for informational purposes only. The views and opinions expressed are those of the author(s) and do not necessarily reflect the official policy or position of Sonic Rocket or its affiliates. Always consult with a certified professional before making any financial or technical decisions based on this content.
Originally published on techdigestor.com. Follow for more developer-focused tooling reviews and productivity guides.
Top comments (0)