---
title: "SES + Lambda: a personal email forwarder that costs nothing"
description: "Want a hello@yourdomain.com that forwards to your Gmail? Most providers charge $5–10/month. Here's the AWS pattern that costs zero and takes 30 minutes."
date_published: 2026-04-19
last_updated: 2026-04-19
canonical: https://vibecodersguidetomvp.help/blog/ses-lambda-email-forwarder/
author: Titan Alpha
tags: ["aws","email","infrastructure"]
---

# SES + Lambda: a personal email forwarder that costs nothing

Want a hello@yourdomain.com that forwards to your Gmail? Most providers charge $5–10/month. Here's the AWS pattern that costs zero and takes 30 minutes.

> Canonical HTML: https://vibecodersguidetomvp.help/blog/ses-lambda-email-forwarder/
> This is the agent-friendly markdown alternate for the page above.


I bought a domain. I needed `hello@my-domain.com` to forward to my actual Gmail inbox. The sensible options:

- **Google Workspace**: $7/user/month. Real Gmail at your domain. Overkill.
- **Fastmail**: $5/user/month. Excellent. Still overkill for forwarding.
- **Domain registrar's free forwarding** (GoDaddy, Namecheap, Cloudflare): free, but the forwarding is unreliable, sometimes mangles SPF/DKIM, and Gmail flags forwarded mail as spam.
- **AWS SES + a tiny Lambda**: free at this volume. Reliable. Mail lands in Gmail's primary inbox.

I picked the last one. Here's the whole pattern. It took me about 30 minutes.

## The shape

```
Sender  →  MX (Route 53)  →  SES inbound  →  S3 PutObject
                                                  ↓
                                              Lambda trigger
                                                  ↓
                                              SES outbound  →  your real inbox
```

Five components. One DNS record set. Twenty lines of Lambda code. The whole pipeline runs on free tier.

## Step by step

**1. Verify the domain in SES.** AWS Console → SES → Verified identities → Create. Pick "Domain." It generates DKIM tokens and a verification token. Drop them into Route 53 as the CNAMEs and TXT record SES tells you to. SES auto-detects when DNS propagates and flips the domain to "Verified."

**2. Verify your destination email** (the one you want forwarded mail to land in). This time you pick "Email address" instead of "Domain." SES sends you a one-click verification link. Click it.

**3. Add MX + SPF to Route 53.** Two records:

   - MX record on the apex: `10 inbound-smtp.us-east-1.amazonaws.com` (or whatever region you're in)
   - TXT record on the apex: `v=spf1 include:amazonses.com ~all`

**4. Create an S3 bucket** for inbound mail (e.g., `my-domain-mail-inbound`). Block all public access. Add a lifecycle rule that deletes objects after 30 days so old mail doesn't accumulate forever.

   Add a bucket policy allowing the SES service principal to write to a `inbox/` prefix:

   ```json
   {
     "Effect": "Allow",
     "Principal": { "Service": "ses.amazonaws.com" },
     "Action": "s3:PutObject",
     "Resource": "arn:aws:s3:::my-domain-mail-inbound/inbox/*",
     "Condition": { "StringEquals": { "AWS:SourceAccount": "<your-account-id>" } }
   }
   ```

**5. Create the Lambda** with an IAM role that can read from the bucket and call `ses:SendRawEmail`. The function code, in Node.js with `mailparser` and `nodemailer`:

   ```ts
   import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
   import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
   import { simpleParser } from 'mailparser';
   import nodemailer from 'nodemailer';

   const FROM_ADDR = 'forwarder@my-domain.com';
   const TO = 'me@gmail.com';

   export const handler = async (event) => {
     const s3 = new S3Client({});
     const ses = new SESv2Client({});
     for (const record of event.Records) {
       const obj = await s3.send(new GetObjectCommand({
         Bucket: record.s3.bucket.name,
         Key: decodeURIComponent(record.s3.object.key),
       }));
       const raw = Buffer.concat(await obj.Body.toArray());
       const parsed = await simpleParser(raw);

       const transporter = nodemailer.createTransport({ streamTransport: true, buffer: true });
       const info = await transporter.sendMail({
         from: { name: parsed.from?.text ?? 'Forwarder', address: FROM_ADDR },
         to: TO,
         replyTo: parsed.from?.value?.[0]?.address,
         subject: parsed.subject ?? '(no subject)',
         text: parsed.text,
         html: parsed.html,
         attachments: parsed.attachments,
       });

       await ses.send(new SendEmailCommand({
         FromEmailAddress: FROM_ADDR,
         Destination: { ToAddresses: [TO] },
         Content: { Raw: { Data: info.message } },
       }));
     }
     return { statusCode: 200 };
   };
   ```

**6. Wire S3 → Lambda.** In the bucket's properties, add an event notification: `s3:ObjectCreated:*`, prefix `inbox/`, target = your Lambda. Give Lambda invoke permission to S3.

**7. Create a SES receipt rule set.** One rule with one recipient (`hello@my-domain.com`) and one action: write to S3 `inbox/`. Activate the rule set.

**8. Test it.** Send an email from any address to `hello@my-domain.com`. Within a few seconds it lands in your real inbox, with the original sender in the Reply-To so you can reply naturally.

That's the entire setup.

## Why use mailparser + nodemailer

I tried the naive version first — read the raw RFC 822 message, do header rewrites with regex, ship it back through SES. It worked, but Gmail rendered the body badly because my hand-rolled MIME boundary handling was wrong. Multipart bodies leaked their headers into the visible text.

The fix is `mailparser` (parses incoming) plus `nodemailer` (composes outgoing) — both battle-tested and exactly what you want for this. The Lambda code stays under 50 lines and Gmail renders the forwarded mail as cleanly as the original.

## The cost math

- **SES inbound**: free for the first 1,000 emails per month, then $0.10 per 1,000.
- **S3 storage**: pennies. Inbound mail is small and the lifecycle rule auto-deletes after 30 days.
- **Lambda**: free tier is 1M invocations + 400,000 GB-seconds per month. A personal email forwarder might see a few hundred invocations a month.
- **SES outbound**: free for emails sent from EC2 (and Lambda counts), $0.10 per 1,000 otherwise. At personal-mail volume, free.

Total: **$0/month** in practice. If your email volume hits 5,000 forwarded messages a month, you'd be at maybe $0.40.

## Why this beats the registrar's free forwarding

GoDaddy and Cloudflare offer "free email forwarding" that works fine for your dad sending you a forward. For anything important, two failure modes show up:

1. **SPF/DKIM mismatch on rewrites.** Forwarders that don't re-sign the outgoing mail get flagged as spam by Gmail because the From header doesn't match the SPF record of the actual sending server. SES lets you DKIM-sign outbound, which keeps you out of spam.

2. **Reliability.** Free forwarders deprioritize your traffic when their systems are under load. SES has the same SLA as the rest of AWS — i.e., you'll never notice.

For a domain you actually use professionally, the AWS pattern is worth the extra 30 minutes.

## When to upgrade

If you grow into needing real email *send* capability for a product (transactional mail, marketing, password reset flows), use **Resend** instead — it's friendlier than SES outbound for app-driven email, has a great dev experience, and is what the Vibe Coder's Guide skills recommend by default.

But for personal forwarding to your inbox, SES + Lambda is the answer. Free, reliable, and you own every component.

