Testing Cognito sign-up emails: the 50-a-day limit and the code

Updated October 17, 2026 · Zyntra

With Cognito's default email, your whole AWS account can send 50 emails a day, so a test suite that signs up real addresses runs out fast. Test real delivery through your own Amazon SES configuration (out of the SES sandbox), and confirm users with AdminConfirmSignUp in the tests where the email isn't what you're testing.

The default email: 50 a day per account

  • Cognito's quota for the default email configuration is 50 email messages a day per AWS account, reset daily at 09:00 UTC. All user pools share it.
  • Messages come from [email protected] unless you verify your own address in SES.
  • Hard bounces go to a suppression list that AWS manages: with the default configuration you can't remove an address from it. Tests that sign up made-up addresses at real domains feed that list.
  • Cognito may also hold back more email to one destination within a short time, so reusing one address across a fast suite can lose emails.

AWS's own docs say the default limit is below what typical production needs. It is also below a test suite that runs a few times a day.

Your SES configuration, and the sandbox

With EmailSendingAccount set to DEVELOPER, Cognito sends through your SES identity and your SES limits apply. A new SES account starts in the sandbox, where it can only send to verified addresses and domains. Until the account is out of the sandbox, Cognito can't email a test inbox you don't own, so request production access for the account staging uses.

The test: SignUp, read the code, ConfirmSignUp

Cognito sends sign-up verification as a code by default (a link is an option for self-registration). The test below runs without a browser: it calls the same API as your sign-up form. It assumes an app client without a client secret and a pool that uses the email as the username.

zyntra.ts
type Email = {
  subject: string;
  body: string;
  html: string | null;
  links: { url: string; text: string }[];
};

// Waits up to 30 s on the server for the first email to the address.
export async function lastEmail(address: string): Promise<Email> {
  const res = await fetch(
    `https://api.zyntra.app/emails/last?email=${encodeURIComponent(address)}`,
    {
      headers: { "X-API-Key": process.env.ZYNTRA_API_KEY! },
      signal: AbortSignal.timeout(45_000),
    },
  );
  if (!res.ok) throw new Error(`No email for ${address}: ${res.status}`);
  return res.json();
}

export const testAddress = (tag: string) =>
  `${process.env.ZYNTRA_TEAM_ID}.${tag}-${Date.now()}@zyntramail.com`;
cognito-signup.spec.ts
import { test, expect } from "@playwright/test";
import {
  CognitoIdentityProviderClient,
  ConfirmSignUpCommand,
  SignUpCommand,
} from "@aws-sdk/client-cognito-identity-provider";
import { lastEmail, testAddress } from "./zyntra";

const cognito = new CognitoIdentityProviderClient({ region: process.env.AWS_REGION });
const ClientId = process.env.COGNITO_CLIENT_ID!;

test("sign-up code arrives and confirms the user", async () => {
  test.setTimeout(90_000);
  const address = testAddress("cognito-signup");

  await cognito.send(new SignUpCommand({
    ClientId,
    Username: address,
    Password: "A-long-test-password-1",
    UserAttributes: [{ Name: "email", Value: address }],
  }));

  const email = await lastEmail(address);
  // Anchor to your template's wording around {####}.
  const codes = [...email.body.matchAll(/code is (\d{6})/gi)];
  expect(codes).toHaveLength(1);

  await cognito.send(new ConfirmSignUpCommand({
    ClientId,
    Username: address,
    ConfirmationCode: codes[0]![1]!,
  }));
});

When the email isn't under test, don't send it

  • AdminCreateUser with MessageAction: "SUPPRESS" creates a user without the invitation email.
  • AdminConfirmSignUp confirms a self-registered user without the code.
  • AdminUpdateUserAttributes with email_verified = true marks the address verified.

Questions

Does the 50-a-day limit apply per user pool?
No. AWS lists it per AWS account, and it applies only to the default email configuration. With your own SES configuration, your SES sending limits apply instead.
Can I test the forgot-password email the same way?
Yes: call ForgotPassword, read the code from the email, then call ConfirmForgotPassword with it and the new password.