Direct answer

How do you wait for an email in CI tests?

Replace fixed sleeps with a bounded, filtered wait that can explain why the message did not produce the expected result.

Maintained by Evidence checked Editorial method

Open the quickstart
Direct answer

Create a unique inbox or tagged recipient for the CI run, record the time immediately before triggering the application email, then poll with backoff, use a provider wait operation, or consume an event until a message matches the recipient, direction, sender context, type, and time boundary. Stop at a fixed deadline. If several messages match, fail as ambiguous. Extract the required code or link, submit it once, and assert the final application state. Keep mailbox credentials, codes, and message bodies out of general CI logs.

Visual guide

Workflow at a glance

Poll narrowly and stop predictably

  1. 01
    Query a small page

    Filter mailbox, recipient, direction, sender context, and received time.

  2. 02
    Classify the result

    Return no candidate, one match, ambiguity, or parse failure.

  3. 03
    Back off with jitter

    Avoid synchronized load across parallel workers.

  4. 04
    Fail at the deadline

    Report the last transport and query evidence without leaking content.

Setup

Allocate the recipient before the test action

Parallel jobs cannot safely share one inbox and select the newest message. Create a mailbox or unique address tag per run, keep its credential scoped, and record the recipient in test state rather than broad logs.

Capture the trigger timestamp immediately before the signup, reset, invitation, or notification action. This prevents an older matching message from satisfying the test.

Wait loop

Poll narrowly and stop predictably

  1. 01

    Query a small page

    Filter mailbox, recipient, direction, sender context, and received time.

  2. 02

    Classify the result

    Return no candidate, one match, ambiguity, or parse failure.

  3. 03

    Back off with jitter

    Avoid synchronized load across parallel workers.

  4. 04

    Fail at the deadline

    Report the last transport and query evidence without leaking content.

Example

Use a purpose-built wait when the task is an OTP

A command such as mails code --to "$TEST_EMAIL" --timeout 60 packages the bounded wait and extraction path. The test should still know which application action triggered the message and should reject a returned value that does not produce verified state.

Do not request a new OTP every time the inbox is empty. Multiple active codes make selection ambiguous and may trigger application rate limits.

Bounded OTP wait
mails code --to "$TEST_EMAIL" --timeout 60
Failure output

Report the failed layer without exposing the mailbox

ResultMeaningNext check
No inbound eventRouting or application send may have failedApplication log and provider event
Message found, no valueTemplate or parser mismatchRedacted fixture and extraction rule
Several matchesIsolation or retry problemRecipient, timestamp, and trigger count
Value rejectedStale code or application ruleFinal endpoint and current message selection
Evidence

Sources and product scope

Product behavior is verified against the mails0 source and documentation. External comparisons link to official vendor documentation checked on 2026-08-15.

Questions and answers

Related questions

How long should a CI test wait for email?

Set a deadline from observed delivery behavior and the suite budget. Report elapsed time and last known state; do not wait indefinitely.

Are webhooks better than polling in CI?

Usually a bounded wait is simpler for a short-lived CI process. Use webhooks when the test infrastructure already has a durable event path and needs that behavior under test.

Why did the test use an old verification code?

The query likely lacked a trigger timestamp, unique recipient, or exact filter. Record the boundary before requesting the code and fail if several current candidates remain.

Start with a bounded inbox

Prove the smallest useful email loop.

Start with one scoped inbox and one expected message. Add durable identity, sending, and operational complexity only when the first loop works.

Open the quickstart