Browser test automation

Waiting for delayed verification mail in Cypress tests

In Cypress, poll for verification mail with bounded retries and clear assertions—never cy.wait(fixed) alone—and tear down inboxes after the run.

Stopwatch beside an envelope arriving on a retry conveyor with a green success stamp

The decision in plain terms

When Cypress must wait for a delayed verification email, use bounded polling against a receive API or test double—not a single long cy.wait(15000). Assert on message content (subject, code shape, link host), then drive the verify step. Tear down users and inboxes in afterEach / after.

Mailby Quick Inbox is fine for manual QA. For automation, call the live developer API yourself (or stub mail in-process). There is no official “Mailby Cypress plugin” required—and inventing CI magic you do not operate will flake in worse ways.

Cypress context and boundaries

Email introduces asynchrony Cypress does not control: provider queues, greylisting, template render, inbound MX. E2E that signup → hard sleep → hope mail arrived is the top source of “works on my machine” auth flakes.

Patterns that stay honest:

  • Prefer contract tests for “we called Resend with the right payload.”
  • Prefer inbound polling for “the user can complete verify.”
  • Keep one E2E path green in staging; do not spray production MX with parallel CI.

This write-up focuses on waiting / retry / assertion / teardown for delayed verification under Cypress—not a full automation hub.

Implementation pattern: poll with a ceiling

Pseudo-custom command:

// cypress/support/commands.js — pattern only
Cypress.Commands.add("waitForVerifyEmail", (inboxId, { timeout = 60000, interval = 2000 } = {}) => {
  const deadline = Date.now() + timeout;
  const poll = () => {
    return cy.request("GET", `${Cypress.env("MAIL_API")}/inboxes/${inboxId}/messages`).then((res) => {
      const hit = (res.body.messages || []).find((m) => /verify/i.test(m.subject));
      if (hit) return cy.wrap(hit);
      if (Date.now() > deadline) throw new Error("verification mail not received before timeout");
      return cy.wait(interval).then(poll);
    });
  };
  return poll();
});

Then in the spec:

it("verifies signup from email link", () => {
  cy.task("provisionInbox").then((inbox) => {
    cy.visit("/signup");
    cy.get("[data-cy=email]").type(inbox.address);
    cy.get("[data-cy=submit]").click();
    cy.waitForVerifyEmail(inbox.id).then((msg) => {
      const url = extractAppLink(msg.text);
      expect(url.hostname).to.eq(new URL(Cypress.env("APP_URL")).hostname);
      cy.visit(url.pathname + url.search);
    });
    cy.get("[data-cy=verified-banner]").should("be.visible");
    cy.task("destroyInbox", inbox.id);
    cy.task("deleteUser", inbox.address);
  });
});

Working path: Mail arrives at 4s; poll succeeds; link host matches; user lands verified.

Failure / limitation: Provider delay exceeds 60s under load → test fails loudly (good) rather than passing on a stale mailbox from a prior run (bad). Increase timeout only with product owners’ SLO awareness—not to hide send bugs forever.

Step table

Test steptimeout/retryassertionteardown
Provision inboxN/Aid + address returneddestroy inbox
Submit signupUI command timeoutsuccess toast / 200delete user
Poll messagese.g. 60s / 2ssubject match; non-empty body
Extract token/linksyncregex; allowlisted host
Visit verify URLCypress defaultverified state in UI/APIclear cookies
Resend flow (optional)separate budgetonly latest token worksinvalidate tokens

Timing edge cases

  • Fixed sleeps: cy.wait(10000) passes when mail is fast and fails randomly when slow—or worse, races ahead and reads an old message if inbox reuse is broken.
  • Inbox reuse: Parallel Cypress machines sharing one address cross-contaminate. Provision unique inboxes per test.
  • Clock: If the app’s token TTL is 5 minutes and CI queue pauses the runner, tokens expire mid-wait. Prefer longer staging TTL or inject time.
  • cy.intercept on outbound mail: Great for asserting the app attempted send; it does not prove inbound delivery. Use both layers intentionally.
  • Flaky DNS / staging ESP: Tag tests @email and allow quarantine without blocking unrelated UI suites.

Deep-dive companion for app-side assertions: Next.js signup verification testing. Retention clocks that affect human debugging: /data-retention.

Measurable assertion examples

Good:

  • expect(msg.subject).to.match(/^Verify your/)
  • expect(code).to.match(/^\d{6}$/)
  • expect(url.origin).to.equal(Cypress.env("APP_URL"))
  • After verify: cy.request("/api/me").its("body.emailVerified").should("eq", true)

Bad:

  • Asserting only that “some email arrived”
  • Clicking the first link in HTML without host checks
  • Reusing codes from screenshots in the repo

Alternatives and when not to use a temporary inbox

  • In-memory outbox in test env — fastest; skips MX entirely for unit/integration.
  • ESP test APIs — assert send payload; pair with fewer full E2E mail tests.
  • Mailby API inboxes — private receive for staging E2E (/developers).
  • Shared durable QA mailbox — human exploratory only; avoid for parallel Cypress.

Permanent shared mailboxes are safer for long-lived review apps where humans must click through for days; they are worse for CI isolation.

Short answers

What causes delayed verification mail in Cypress?
Real queues and network; also intentional provider delays and greylisting.

What should I do first?
Replace fixed sleeps with bounded poll + content assertions; unique inbox per test.

When is a permanent address safer?
Manual UAT on a shared staging login—not parallel CI.

What evidence changes the recommendation?
Your send path is fully mocked and product risk is covered by contract tests—then drop E2E mail waits.

Why verification mail latency is heavy-tailed

In staging you often see delivery under two seconds. In CI, the same provider may take 20–40 seconds under shared IP reputation, cold start, or regional routing. Design timeouts for the tail, not the median—then alert if the tail grows beyond product SLOs.

Record timings in the test report (mailWaitMs). Trends tell you whether flakes are app regressions or provider delays.

Anti-patterns checklist

  • cy.wait(30000) with no assertion on content
  • Reading “latest message” without filtering by recipient or subject
  • Shared inbox across shards
  • Clicking tracking redirects without host allowlists
  • Storing API tokens in the repo
  • Retrying the whole spec 5 times to “green” a broken send path

Hybrid strategy: intercept + poll

Use cy.intercept to assert the browser/UI triggered signup successfully and that any client-visible resend works. Separately poll inbound mail. If intercept shows send API 500, fail fast without waiting the full mail timeout. If intercept shows 200 but poll times out, fail with a distinct error string so on-call knows where to look.

Seeded delay drills

Occasionally inject artificial delay in staging send (queue pause) to prove your poller survives. If the test cannot handle a 15-second delay, it will not handle Monday morning ESP lag.

Reporting UX for failures

Custom error:

Verification mail not found for inbox X after 60s. Last subjects: [...]

beats

Timed out retrying.

Include inbox id (not secrets) in CI logs for human follow-up via developer console.

Relationship to manual Quick Inbox

Manual QA remains valuable for HTML rendering and mobile clients. Cypress proves the automated path. Use /inbox when exploring a new template; use API polling when locking the suite.

Designing timeouts from product SLOs

If product claims “email arrives within 2 minutes,” a 60-second test timeout will false-fail. Align:

  • User-visible copy
  • Support playbooks
  • Cypress timeout

If you cannot meet a two-minute SLO in staging, fix sending before greenwashing CI with 10-minute waits.

Quarantine and ownership

Tag @email tests. When they fail, page the team that owns auth/email—not random frontend on-call. Publish a dashboard of mailWaitMs p50/p95 weekly.

Local vs CI credentials

Developers may use Quick Inbox manually. CI should use API tokens with least privilege, separate from production. Destroy inboxes aggressively to avoid quota exhaustion.

Flake classification rubric

  • App bug: send never called; wrong template
  • Provider issue: 5xx; deferrals
  • Test bug: shared inbox; wrong regex; short timeout
  • Env bug: DNS; blocked egress from CI

Require classification in the failure ticket; “flaky email” is not a root cause.

Complementary reading

App assertions: Next.js signup verification. Manual receive: /inbox. Retention while debugging: /data-retention. Features overview: /features.

Closing operational tip

If a single Cypress run must validate mail on a Monday morning after ESP incidents, allow a longer timeout via env EMAIL_WAIT_MS rather than editing the spec. Keep the default tight so regressions surface quickly on normal days.

Network egress and CI runners

Some CI environments block outbound SMTP or restrict which hosts can be reached. Prefer HTTPS APIs to your ESP and to Mailby’s developer endpoints over speaking SMTP from Cypress. Document required allowlist domains in the repo. When a job cannot reach the mail API, fail with ECONNREFUSED clarity rather than polling until timeout.

For open-source projects without secrets, mock inbound mail entirely in public CI and run real-mail E2E nightly in a private pipeline.

Add a smoke job that only checks GET /health on the mail API before the heavy suite runs—saving minutes when credentials expire.

Consider recording a short loom for new hires showing one green email E2E; tribal knowledge is how timeouts get cargo-culted.

Choosing poll interval

Too aggressive (100ms) hammers APIs and trips rate limits. Too slow (10s) wastes CI wall clock after fast deliveries. Start at 1–2s with jitter. Back off after successive empty polls if your mail API supports it. Cap total attempts explicitly (maxPolls) in addition to wall-clock timeout so runaway recursion cannot hide in a custom command.

Log poll attempt counts in the Cypress screenshot/video annotation when failing—future you will thank present you.

Idempotent teardown patterns

cy.task('destroyInbox') should succeed if the inbox is already gone. Same for user delete. Idempotent teardown keeps failed tests from cascading into “user already exists” on retry.

Spec structure that stays maintainable

Split files:

  • auth.signup.email.cy.js — the one true E2E mail path
  • auth.signup.mocked.cy.js — UI-only with intercepts
  • auth.verify.unit — outside Cypress

Reviewers should reject new specs that copy-paste poll commands with divergent timeouts. Centralize waitForVerifyEmail in support.

On failure, dump the last HTTP status from the mail API and the list of subject lines seen. That artifact converts “flaky” into “provider 503 for 40s.”

For mobile viewport Cypress runs, email steps are usually unchanged—but verify pages may break. Keep mail polling independent of viewport so you do not debug CSS while waiting on MX.

If your team uses Cypress Cloud, tag retries and compare mailWaitMs across attempts; rising wait on retry #2 often means inbox contamination, not random flake.

Document for contractors: never point Cypress at production Mailby user inboxes; use developer API projects meant for CI.

Reader takeaway

Match the mailbox lifetime to the longest message you still need. For this article’s scenario, that rule decides temporary versus durable more reliably than any generic “always use temp mail” tip. Re-read your vendor’s security and billing emails settings after signup so the address you chose still matches the account’s real role.

Sources, test date, limitations

  • Cypress best practices on waiting and avoiding arbitrary waits (official Cypress docs on best practices).
  • RFC 5321 — delivery is asynchronous by nature.

Date: 2026-09-24. Exact Mailby API shapes belong in current developer docs at /developers. No claim of zero-flake email E2E on the public internet.

Conclusion

Delayed verification mail needs polling, strict assertions, and ruthless cleanup—not longer sleeps. Prove the path once with Quick Inbox, automate with explicit API calls, and keep Mailby’s role honest: receive-only infrastructure for tests you design and own.

Try it on Mailby

Open a receive-only disposable inbox when a short-lived address fits the job — session-bound, with timed purge.