The whole point of serverless is no server. No long-lived process, no persistent socket, no local Postfix. Every email has to leave via an HTTPS API call to an ESP — SES, Mailgun, Postmark, SendGrid, Resend. That's the easy part. Deliverability is still on you: DKIM alignment, domain reputation, content, rate-limits, and the places where cold starts quietly break things.
This article covers the three big serverless platforms — AWS Lambda, Vercel Functions, Cloudflare Workers — with the path, the pitfalls, and working code snippets for each.
Why serverless email is different
- No persistent SMTP socket. Every invocation opens a new HTTPS connection. For low volume fine; for high volume, connection setup dominates and timeouts cascade.
- Cold start kills retry logic. If your function times out in the middle of a send-retry loop, the retry never happens. You need external queueing (SQS, Cloudflare Queues) for anything transactional.
- Secrets live outside the runtime. DKIM keys don't belong in your function — they belong at the ESP. You never touch them.
- Observability is harder. No
tail -f /var/log/mail.log. You need CloudWatch/Vercel logs + the ESP's own dashboard, and they don't agree on format. - Your domain is still yours. SPF, DKIM (at the ESP), DMARC, alignment — all still your responsibility.
AWS Lambda + SES
The native path. SES is AWS's ESP; Lambda has IAM roles that let it call SES without credentials in code. SES domain verification is clean; DKIM is auto-generated by SES and you publish three CNAMEs.
// handler.ts (Node.js Lambda, AWS SDK v3)
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
const ses = new SESv2Client({ region: 'eu-west-1' });
export const handler = async (event: {
to: string;
subject: string;
html: string;
}) => {
const cmd = new SendEmailCommand({
FromEmailAddress: 'hello@yourdomain.com',
Destination: { ToAddresses: [event.to] },
Content: {
Simple: {
Subject: { Data: event.subject },
Body: { Html: { Data: event.html } },
},
},
ConfigurationSetName: 'transactional', // for event destinations
});
try {
const out = await ses.send(cmd);
return { messageId: out.MessageId };
} catch (err) {
// Transient? Re-queue to SQS. Hard fail? Log and drop.
throw err;
}
};# serverless.yml - give the function SES permissions
service: txn-email
provider:
name: aws
runtime: nodejs20.x
region: eu-west-1
iam:
role:
statements:
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
- sesv2:SendEmail
Resource: '*'
functions:
send:
handler: handler.handler
timeout: 10
memorySize: 256
events:
- sqs:
arn: arn:aws:sqs:eu-west-1:123456789012:email-queue
batchSize: 10Trigger via SQS, not direct API Gateway. SQS gives you automatic retries with DLQ for failures; API Gateway just drops them.
SES deliverability knobs
- Move out of the SES sandbox before production (request in console).
- Verify your sending domain — publish the three DKIM CNAMEs SES gives you.
- Add
include:amazonses.comto your domain's SPF. - Request a dedicated IP if sending >50k/month — shared SES IPs have mixed reputation.
- Enable SES virtual deliverability manager for placement / complaint tracking.
Vercel Functions + Resend (or SendGrid)
Vercel has no native ESP. Resend was founded by Vercel alumni and has best-in-class React Email integration, so it's the path of least resistance. SendGrid and Postmark work equally well via HTTPS.
// app/api/send/route.ts (Vercel Next.js route handler)
import { Resend } from 'resend';
import { NextResponse } from 'next/server';
const resend = new Resend(process.env.RESEND_API_KEY);
export const runtime = 'nodejs'; // or 'edge' - see below
export async function POST(req: Request) {
const { to, subject, html } = await req.json();
try {
const { data, error } = await resend.emails.send({
from: 'hello@yourdomain.com',
to,
subject,
html,
headers: {
'X-Entity-Ref-ID': crypto.randomUUID(), // for your logs
},
});
if (error) return NextResponse.json({ error }, { status: 500 });
return NextResponse.json({ messageId: data?.id });
} catch (err) {
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}Edge runtime has smaller SDK compatibility and no Buffer/crypto quirks. Resend's SDK supports both; SendGrid's Node SDK does not work on Edge. For max compatibility, use Node runtime unless you have a latency reason to go Edge.
Vercel deliverability knobs
- Vercel doesn't send mail itself — the Vercel dashboard's "deployment notification" email uses Resend under the hood.
- All auth is at your ESP (Resend, SendGrid, Postmark). Publish their DKIM/return-path records.
- For long-running send jobs, use Vercel Queues or an external queue (Upstash, Inngest). Function timeout is 10s on Hobby, 60s on Pro; a big campaign doesn't finish in one invocation.
Cloudflare Workers + MailChannels / Resend
Workers can't open outbound TCP (no SMTP). You either use MailChannels (historically free for Workers, now partial paid) or call an ESP's HTTPS API. Resend, SendGrid, Postmark all work over fetch().
// worker.ts (Cloudflare Workers with Resend via fetch)
export interface Env {
RESEND_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { to, subject, html } = await request.json<{
to: string; subject: string; html: string;
}>();
const resp = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'hello@yourdomain.com',
to,
subject,
html,
}),
});
if (!resp.ok) {
const err = await resp.text();
return new Response(JSON.stringify({ error: err }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
const data = await resp.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
});
},
};# wrangler.toml
name = "send-mail"
main = "worker.ts"
compatibility_date = "2027-01-01"
[vars]
# public env vars (non-secret)
# Secrets: wrangler secret put RESEND_API_KEY
[[queues.consumers]]
queue = "email-queue"
max_batch_size = 10
max_batch_timeout = 5Workers deliverability knobs
- No TCP egress means no SMTP. Period. Use HTTPS APIs only.
- Cloudflare Email Routing handles inbound only — it doesn't send.
- MailChannels was the de-facto free path for Workers but tightened pricing in 2024. Check current terms.
- Use Cloudflare Queues for retry; Worker CPU time is capped, big bursts overflow.
What's the same across all three
- Your domain owns the reputation. SPF, DKIM at the ESP, DMARC — same as any other setup. A clean ESP account with a dirty sending domain still lands in Spam.
- From address must match DKIM domain. Sending
From: hello@yourdomain.comwith DKIMd=sesmail.amazonses.comis alignment-fail — DMARC rejects. Always verify your own domain at the ESP. - Rate-limits are per-account at the ESP. Serverless can burst; most ESPs throttle gracefully, but if you blast 100k/min you'll hit 429 and your function queue backs up.
- Observability needs effort. Log messageId + recipient + template-id to CloudWatch/Logpush; correlate with the ESP's delivery webhook. Without this you can't answer "did that password reset send?"
- Placement testing still applies. Before shipping a new template, run it through a seed-mailbox check. Serverless doesn't change spam filters at Gmail — they grade on content + reputation regardless of origin.
How to pick an ESP for serverless
- SES: cheapest at scale, best if you're already AWS-native. Sharp edges: sandbox mode, shared IPs with mixed reputation, complex bounce/complaint handling.
- Postmark: best deliverability out of the box for transactional. Expensive per-message but worth it for receipts, password resets.
- Resend: great DX for Next.js/React shops. Good deliverability, newer platform.
- SendGrid: universal SDK support, everything-for-everyone. Shared IPs can be noisy; dedicated IP at higher plans.
- Mailgun: solid, used to be indie-friendly, now requires credit card on sign-up. Good for transactional & marketing split configs.
Before a new template goes live, run it from your function against 20+ seed addresses. Inbox Check gives you free seed mailboxes across Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail. Read the placement + auth headers before a real user ever sees it.