Resend Transactional Email Done Right in 2026
Production patterns for Resend v3+: setup, error handling, templates, and avoiding common pitfalls developers hit in console.
TL;DR
Resend is a developer-first transactional email service. This guide covers v3.0+ setup, real error patterns, production-ready code, and why it's worth considering over legacy SMTP. Verify current pricing and rate limits in [official Resend docs](https://resend.com/docs).
---
Why Transactional Email Still Matters
You ship features. Users need confirmations, password resets, invoices, and notifications. SMTP is dead for most startups—it's slow, unreliable, and ops-heavy. Resend handles the infrastructure: bounce management, deliverability scoring, DKIM/SPF/DMARC setup.
But "done right" means understanding the actual patterns developers struggle with.
---
The Setup (Resend v3+)
Verify current version in official docs before implementing.
```bash npm install resend@latest
or
pip install resend ```Get your API key from [resend.com/api-keys](https://resend.com/api-keys). Treat it like a database credential—use environment variables.
```javascript // .env.local (never commit this) RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxx ```
```javascript // lib/email.ts - Production pattern import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendTransactionalEmail({ to, subject, html, replyTo = 'support@yourdomain.com', tags = [], }: { to: string | string[]; subject: string; html: string; replyTo?: string; tags?: string[]; }) { try { const response = await resend.emails.send({ from: 'noreply@yourdomain.com', to, subject, html, reply_to: replyTo, tags, // Track by category: ["signup"], ["password-reset"] });
if (response.error) {
console.error('Resend error:', response.error);
throw new Error(Email send failed: ${response.error.message});
}
return response.data; } catch (error) { console.error('Transactional email error:', error); throw error; // Let your error boundary handle it } } ```
Why this pattern?
{error} objects)reply_to is critical—users expect to reach humans---
Real Error Messages You'll See
Developers hit these in console every week:
Error #1: Invalid from Address
``` Error: Invalid from address. Please use a verified domain. Message: The email address you're using hasn't been verified in Resend. ```
Fix: Verify your domain in Resend dashboard. Use DNS records they provide. Takes 5 minutes but catches everyone first.
Error #2: Missing API Key
``` ERR_RESEND_API_KEY_MISSING Error: API key is missing. Pass it to the Resend constructor or set the RESEND_API_KEY environment variable. ```
Fix: Check .env.local is loaded in your runtime. In Next.js, variables need NEXT_PUBLIC_ prefix only if used client-side (email sending should be server-only, so it doesn't).
Error #3: Rate Limit
``` HTTP 429 Too Many Requests Message: You've exceeded your rate limit. Current plan allows X emails/second. ```
Fix: Implement exponential backoff. Verify your plan in docs—free tier is rate-limited, pro tier has higher throughput.
---
HTML Templates: Do It Right
Don't inline HTML strings. Use a template engine or pre-built components.
```typescript // lib/email-templates.ts import { render } from '@react-email/render'; import EmailLayout from '@/emails/layout'; import PasswordResetEmail from '@/emails/password-reset';
export async function sendPasswordReset(
email: string,
resetToken: string,
expiresIn: number
) {
const resetUrl = ${process.env.NEXT_PUBLIC_APP_URL}/auth/reset?token=${resetToken};
const html = render( <EmailLayout> <PasswordResetEmail resetUrl={resetUrl} expiresInMinutes={expiresIn / 60} /> </EmailLayout> );
return sendTransactionalEmail({ to: email, subject: 'Reset your password', html, tags: ['password-reset'], }); } ```
Why React Email?
---
Batch Sending (But Be Careful)
```typescript
// Good: Send to multiple users with individual personalization
export async function sendBatchEmails(
recipients: { email: string; name: string }[]
) {
const results = await Promise.allSettled(
recipients.map(({ email, name }) =>
sendTransactionalEmail({
to: email,
subject: Hi ${name}, here's your invoice,
html: renderInvoiceTemplate({ name }),
tags: ['invoice-batch'],
})
)
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.warn(${failed.length} emails failed to send);
// Log to Sentry or your error tracker
}
return results; } ```
Critical: Don't send one email to 1000 to addresses. That's a list send, not transactional. Resend batches internally—use individual to addresses.
---
Monitoring & Debugging
Resend dashboard shows delivery status, bounce rates, and complaint rates. But add observability to your code:
```typescript // Instrument your email function export async function sendTransactionalEmail({ to, subject, html, replyTo = 'support@yourdomain.com', tags = [], }: Parameters[0]) { const startTime = Date.now();
try { const response = await resend.emails.send({ from: 'noreply@yourdomain.com', to, subject, html, reply_to: replyTo, tags, });
if (response.error) throw response.error;
// Log success to analytics analytics.track('email.sent', { messageId: response.data?.id, recipients: Array.isArray(to) ? to.length : 1, tags, duration: Date.now() - startTime, });
return response.data; } catch (error) { analytics.track('email.failed', { error: String(error), tags, duration: Date.now() - startTime, }); throw error; } } ```
This helps you catch bugs before users complain.
---
Pricing & When to Use Resend
Verify in [official docs](https://resend.com/pricing) for 2026 rates. Generally:
Use Resend if:
Don't use if:
---
Related Patterns
Check our guides on [email validation pipelines](/?guide=email-validation) and [async job queues for email](/?guide=job-queues).
---
What am I missing?
This is 2026—what's changed in Resend's API? Have you hit errors not listed here? Struggling with bounce handling or DMARC alignment? Drop corrections and war stories in the comments. Accuracy matters.
---