SES sandbox: the gotcha nobody reads

Christian Drexler

Amazon SES is the cheapest reliable way to send transactional mail, which is why every sign-up flow I build ends up on it. It also starts every new account in a state called the sandbox, and the sandbox is designed to let your own tests pass while real mail silently fails. I have lost a day to this twice. The second time I wrote it down.

What the sandbox does

In sandbox mode SES will only deliver to addresses and domains you have verified in the console. You verify your own address first, of course, because that is how you test. So you test, the mail arrives, everything looks fine. Then a real person signs up, the send call returns an error with the text Email address is not verified, and your code either logs it somewhere nobody looks or swallows it entirely. From the outside the sign-up form looks healthy. The user just never gets the confirmation.

The sandbox also caps you at 200 messages per 24 hours and one message per second. Both limits are low enough to trip a small launch and high enough that a test never touches them.

How to notice before it bites

Do not rely on the first failing send. Check the account state directly, in the region you actually send from, because the sandbox is per region.

aws sesv2 get-account --region eu-central-1 \
  --query '{production: ProductionAccessEnabled, quota: SendQuota}'

If production comes back false, you are in the sandbox, whatever your test inbox says. I run this line in the pre-launch checklist of anything that sends mail, next to the DKIM and SPF checks. It costs nothing and it has caught the problem once already.

The console says the same thing on the SES account dashboard, with a yellow banner, which is exactly the kind of banner you scroll past when you are busy verifying a domain.

Getting out

Production access is a request, not a toggle. In the SES console there is a "Request production access" button; it opens a short form that asks what you send, roughly how much, how recipients opted in, and how you handle bounces and complaints. Answer it plainly. "Transactional mail for a sign-up confirmation, around 300 per day, double opt-in, bounces and complaints go to an SNS topic that suppresses the address" is a complete answer. AWS usually replies within a working day, sometimes within hours. If they ask a follow-up question, answer it the same day, because the case goes quiet otherwise.

Have the bounce handling in place before you ask. They do check, and the easiest way to get a slow approval is to say you will set it up later.

The two things I now do by default

First, the account check above goes into the launch checklist, in every region the service uses. Second, the send function treats MessageRejected as a hard failure that pages me rather than a soft one that gets logged. A confirmation mail that does not arrive is a lost sign-up, and a lost sign-up should be loud.

If you are about to put something live on a fresh AWS account, request production access on day one, before you need it. It is the cheapest thing on the list and the one that costs the most when forgotten.