Cutting MX records to SES when your domain loops back to itself
I had cut over the MX records on several personal domains to AWS SES, because I wanted every incoming email going through a Lambda that writes to DynamoDB and forwards to Google Workspace. Worked fine for most domains. One domain refused to play.
The setup
For each domain:
MX 10 inbound-smtp.us-east-1.amazonaws.com
SES inbound rule writes the raw .eml to S3, runs a Lambda that
parses the MIME into DynamoDB, and forwards via SES outbound to
<address>@<domain>. For most of my domains, the destination
address is on Google Workspace, so the SES outbound DNS lookup
finds Google's MX records and the message lands in my mailbox.
The loop
For the one domain that's also the receive address on Google Workspace, the cutover created a self-loop:
SES inbound (example.com)
→ Lambda
→ SES outbound looks up MX for example.com
→ "10 inbound-smtp.us-east-1.amazonaws.com" — that's us
→ SES inbound (example.com)
→ Lambda
→ ... forever
Every inbound message would generate an infinite chain of duplicate deliveries. Lambda would happily run a few thousand invocations a minute against a single message. Not great.
Options I considered
A. Alias subdomain. Stand up gw.example.com, point its MX at
Google Workspace, change the Lambda to forward to
<address>@gw.example.com. Workspace catch-all routing lands it
in the real mailbox. The example.com MX can stay on SES.
B. Gmail API insert. Use the Gmail API to users.messages.insert
directly into my Workspace mailbox, bypassing SMTP entirely. Heavier
auth (Workspace service account, domain-wide delegation), but no
loop because no SMTP involved on the destination side.
C. Keep example.com MX on Google. Lose the Dynamo archive
for this one domain. Simplest, but defeats the point.
What I picked
Option A. Five minutes in the Workspace admin console — add the domain alias, the catch-all route already handles the rest. Lambda config change is one environment variable. Zero new auth surface.
The other domains have no loop risk because they don't share names with Google Workspace destinations, so their MX cutover stayed unchanged.
What I'd do differently
I should have caught the loop in design, not deployment. The question to ask before any "forward via SES" architecture: for each domain you forward to, does its MX resolve to your own SES? Walk the destination MX chain once, before cutover, and the loop is obvious.
Lambda concurrency limits would have eventually choked the runaway loop, but they're a circuit breaker, not a design. SES inbound bills per-message even when the destination loops; a loop running unattended for a day could rack up real dollars before the metrics made it obvious.