DEV Community

Silviu Technology
Silviu Technology

Posted on

Lease Inboxes in Parallel Playwright Tests

Parallel Playwright runs are great until three workers start reading from the same inbox pool and your failure output becomes fiction. The app may be healthy, delivery may be healthy, yet one test still grabs another test's message and claims the suite is broken. I used to treat that as "email flake." Now I treat it as an ownership bug.

The fix that has held up best for me is an inbox lease. Before a test asks the product to send mail, it claims one inbox for a short window, records the owner, and refuses to read anything that does not belong to that lease. This sounds like extra ceremony, but it makes retries much easier to trust.

Why parallel email tests steal each other's evidence

Most flaky email suites fail for plain reasons:

  • worker A and worker B share the same disposable inbox source
  • the poller finds the newest email, not the correct email
  • cleanup runs late, so old messages still look fresh
  • failure logs show the subject line but not the ownership trail

When that happens, the test report is misleading. You end up debugging the assertion harness instead of the product. I have seen teams burn an hour on a "delivery regression" that was really a stale mailbox record from a prior run.

This is close to the problem behind state-bound recovery email checks: once state is loose, the email itself is no longer strong evidence. Isolation matters more than heroic timeout tuning.

I also keep an eye on the junk phrases people type during incidents. Someone always searches for fake e mail com or tepm mail com in logs and notes when they are rushing. If your tooling cannot survive messy real-world search habits, it is more fragile than it looks.

What I mean by an inbox lease

An inbox lease is just a short contract:

  1. Allocate one inbox for one test owner.
  2. Stamp it with a lease ID and expiry.
  3. Allow reads only while the lease is active.
  4. Release or archive it as soon as the assertion is done.

That structure helps in two ways. First, it reduces cross-test contamination. Second, it gives you a better failure story. If the email never arrives, you can say which lease waited, for how long, and whether another worker touched the same inbox. That is much more usefull than "expected message not found."

For teams using a temporary disposable mail provider in staging, I like to wrap lease creation behind one helper and keep the provider-specific part tiny. Even a simple tempmail.so integration can be fine if you track lease ownership and expiry rather than treating inboxes like a shared bucket.

A Playwright pattern for leased inboxes

Here is the shape I reach for:

import { test, expect } from "@playwright/test";

test("welcome email belongs to this worker", async ({ page }) => {
  const lease = await leaseInbox({
    suite: "signup",
    workerId: test.info().workerIndex,
    ttlMs: 45_000,
  });

  await page.goto("/signup");
  await page.getByLabel("Email").fill(lease.address);
  await page.getByLabel("Password").fill("SuperSecret123!");
  await page.getByRole("button", { name: "Create account" }).click();

  const email = await waitForLeasedEmail({
    inboxId: lease.inboxId,
    leaseId: lease.id,
    timeoutMs: 30_000,
  });

  expect(email.to).toContain(lease.address);
  expect(email.metadata.leaseId).toBe(lease.id);
  expect(email.html).toContain("/verify-email");
});
Enter fullscreen mode Exit fullscreen mode

What matters is the contract, not my helper names:

  • the lease object has a stable ID
  • the poller checks lease ownership, not just arrival time
  • the timeout is explicit and short enough to expose regressions
  • the assertion proves recipient plus intended action

That same mindset works well with golden traces for email regressions. I want a narrow, repeatable evidence trail instead of a huge blob of raw HTML and vague timestamps.

Debug signals that make flaky runs shorter

If a suite still flakes after adding leases, I check four signals first:

  • how many active leases existed when the test started
  • whether the lease expired before polling finished
  • whether any worker reused the same inbox label
  • whether the report printed lease ID, worker ID, and first matching timestamp

These signals make triage faster because they separate product delay from harness confusion. They also stop a common anti-pattern: extending every timeout when one branch goes red. Longer waits can hide mailbox routing bugs for weeks, and then they return at the worst time.

My current rule is simple: if a retry uses the same inbox, I do not trust the retry result very much. A fresh lease is cheaper than a false green, and the maintenance cost is smaller than people think.

Q&A

Do I need a lease if every test already creates a new address?

Usually yes. A fresh address helps, but a lease also records ownership and expiry. That extra metadata is what turns a confusing failure into a debuggable one.

What should expire first: the lease or the test timeout?

I prefer the lease to outlive the polling window by a few seconds. If both expire at the same moment, logs get weird and your cleanup story gets muddy.

What is the smallest useful version of this pattern?

Start with a lease ID, expiry timestamp, and one failure log line that prints both. Even that small step tends to cut down a lot of "it failed, but not really sure why" mornings.

Top comments (0)