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.
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`;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
AdminCreateUserwithMessageAction: "SUPPRESS"creates a user without the invitation email.AdminConfirmSignUpconfirms a self-registered user without the code.AdminUpdateUserAttributeswithemail_verified = truemarks the address verified.