Running 100 Playwright Tests in Parallel Without Inbox Collisions

Running 100 Playwright Tests in Parallel Without Inbox Collisions

如何在并行运行 100 个 Playwright 测试时避免收件箱冲突

If you’ve ever tried to run a large Playwright test suite in parallel — the kind that tests email verification flows, magic links, or password resets — you’ve probably hit this problem: Two tests run at the same time. Both sign up with the same test email address. Test A waits for a verification email. Test B’s email arrives first. Test A reads it. Test B times out. The whole suite goes red, and you spend an hour debugging a race condition that only happens in CI. This is the inbox collision problem. It’s subtle, it’s intermittent, and it’s completely avoidable. 如果你曾尝试并行运行大型 Playwright 测试套件(例如测试电子邮件验证流程、魔术链接或密码重置),你可能遇到过这个问题:两个测试同时运行,都使用同一个测试邮箱地址进行注册。测试 A 等待验证邮件,结果测试 B 的邮件先到达,测试 A 读取了它,导致测试 B 超时。整个测试套件变红,你不得不花一小时去调试这种只在 CI 环境中出现的竞态条件。这就是“收件箱冲突”问题。它隐蔽、间歇性出现,但完全可以避免。

Why shared inboxes fail in parallel CI

为什么共享收件箱在并行 CI 中会失败

Most email testing tools give you one of two options: 大多数电子邮件测试工具提供以下两种方案之一:

Option 1: A single shared inbox (Mailpit, MailHog, local SMTP) All tests funnel into the same inbox. The first test that polls gets whatever email arrived most recently — which might belong to a completely different test. In parallel builds, this is a guaranteed race condition. 方案 1:单个共享收件箱(Mailpit、MailHog、本地 SMTP) 所有测试都汇入同一个收件箱。第一个轮询的测试会获取最近到达的任何邮件——这可能属于完全不同的另一个测试。在并行构建中,这必然会导致竞态条件。

Option 2: A limited pool of inboxes Better, but still breaks when your parallel worker count exceeds the inbox pool. A 20-worker matrix build against a 10-inbox limit means half your tests are fighting over shared state. 方案 2:有限的收件箱池 这比方案 1 好,但当并行工作节点数量超过收件箱池容量时依然会崩溃。如果 20 个工作节点的矩阵构建面对 10 个收件箱的限制,意味着有一半的测试在争抢共享状态。

The real fix is simpler: one isolated inbox per test run, always. 真正的解决方法更简单:始终为每个测试运行提供一个独立的收件箱。

Zero cross-test contamination by default

默认实现零测试间污染

ZeroDrop generates a new inbox on every call — no shared state, no pools, no configuration: ZeroDrop 在每次调用时都会生成一个新的收件箱——没有共享状态,没有资源池,无需配置:

const mail = new ZeroDrop();
const inbox = mail.generateInbox(); // void-a3k9x@zerodrop-sandbox.online

Each inbox name is a random adjective + 7-character alphanumeric string. The address space is large enough that collision probability across thousands of parallel runs is effectively zero. Every test run gets a cryptographically isolated inbox that no other test can see or contaminate. This isn’t a setting you enable. It’s how the system works by default. 每个收件箱名称由一个随机形容词加上 7 位字母数字字符串组成。地址空间足够大,使得数千次并行运行中的冲突概率几乎为零。每个测试运行都会获得一个加密隔离的收件箱,其他测试无法查看或污染它。这不是需要开启的设置,而是系统默认的工作方式。

Parallel matrix builds in GitHub Actions

GitHub Actions 中的并行矩阵构建

Here’s a real example — 4 parallel workers, each running a separate auth flow test, each with its own isolated inbox: 这是一个真实的示例——4 个并行工作节点,每个节点运行一个独立的身份验证流程测试,且每个节点都有自己独立的收件箱:

name: E2E Auth Tests (Parallel)
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4] # Run 4 workers in parallel
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      # Each worker gets its own isolated inbox
      - name: Generate isolated test inbox
        id: inbox
        uses: zerodrop-dev/create-inbox@v1
      - name: Run Playwright shard
        run: npx playwright test --shard=${{ matrix.shard }}/4
        env:
          TEST_INBOX: ${{ steps.inbox.outputs.inbox }}

Four workers. Four inboxes. Zero collisions. The zerodrop-dev/create-inbox Action runs on each worker independently — no shared state, no coordination required. 四个工作节点。四个收件箱。零冲突。zerodrop-dev/create-inbox Action 在每个工作节点上独立运行——无需共享状态,无需协调。

What this looks like at scale

大规模应用场景

The pattern scales linearly. 10 workers, 10 inboxes. 100 workers, 100 inboxes. There’s no pool to exhaust, no lock to acquire, no cleanup step between runs. 这种模式可以线性扩展。10 个工作节点对应 10 个收件箱,100 个工作节点对应 100 个收件箱。没有资源池需要耗尽,没有锁需要获取,运行之间也不需要清理步骤。

// In your Playwright test — works identically across all parallel workers
import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';

const mail = new ZeroDrop();
// process.env.TEST_INBOX is set per-worker by the GitHub Action
// Falls back to a fresh inbox when running locally
const inbox = process.env.TEST_INBOX ?? mail.generateInbox();

test('email verification flow', async ({ page }) => {
  await page.goto('/signup');
  await page.fill('[name="email"]', inbox);
  await page.click('[type="submit"]');

  // This worker's inbox — no other worker can see this email
  const email = await mail.waitForLatest(inbox, { timeout: 15000 });
  const link = email.body.match(/https?:\/\/\S+verify\S+/)?.[0];
  await page.goto(link);
  await expect(page).toHaveURL('/dashboard');
});

Each worker is completely self-contained. The test doesn’t know or care how many other workers are running. 每个工作节点都是完全自包含的。测试本身不需要知道也不关心有多少其他工作节点正在运行。

The inbox pool problem at scale

大规模下的收件箱池问题

Some email testing tools cap concurrent inboxes on lower-tier plans — commonly 10 per account. If your CI matrix runs more than 10 parallel workers, which is common in enterprise pipelines, you either hit the limit and tests fail, or you upgrade to a higher tier for unlimited inboxes. 一些电子邮件测试工具在低级计划中限制并发收件箱的数量——通常每个账户 10 个。如果你的 CI 矩阵运行超过 10 个并行工作节点(这在企业级流水线中很常见),你要么会触及限制导致测试失败,要么必须升级到更高级别以获得无限收件箱。

ZeroDrop’s free tier has no inbox limit. Every test run generates a fresh inbox instantly, at the edge, with no network request during generation. The inbox name is computed locally on the runner — there’s nothing to hit a rate limit on. ZeroDrop 的免费层级没有收件箱限制。每次测试运行都会在边缘节点即时生成一个新的收件箱,生成过程中无需网络请求。收件箱名称是在运行器上本地计算的——没有任何东西会触发速率限制。

Inbox isolation properties

收件箱隔离特性

Each ZeroDrop inbox is isolated by design: 每个 ZeroDrop 收件箱在设计上都是隔离的:

  • Unique per run — random name generated at test start, never reused

  • Ephemeral — auto-deleted after 30 minutes via Redis TTL

  • Private — only accessible via the exact inbox name; no enumeration API

  • Edge-routed — emails are caught at Cloudflare’s global edge, not a central server

  • 每次运行唯一 —— 在测试开始时生成随机名称,永不重复。

  • 临时性 —— 通过 Redis TTL 在 30 分钟后自动删除。

  • 私密性 —— 只能通过确切的收件箱名称访问;没有枚举 API。

  • 边缘路由 —— 邮件在 Cloudflare 的全球边缘节点捕获,而非中心服务器。

The 30-minute TTL means stale test data never accumulates. A test suite that ran 6 hours ago has left zero traces. 30 分钟的 TTL 意味着陈旧的测试数据永远不会堆积。6 小时前运行的测试套件不会留下任何痕迹。

Working example

运行示例

A complete parallel Playwright setup with ZeroDrop, including the GitHub Actions matrix configuration and full auth flow tests: 一个完整的 ZeroDrop 并行 Playwright 设置,包含 GitHub Actions 矩阵配置和完整的身份验证流程测试:

github.com/zerodrop-dev/zerodrop-playwright-example

The example uses a single worker for simplicity, but the pattern scales directly to matrix builds — just add the strategy.matrix block shown above. 为了简单起见,该示例使用单个工作节点,但该模式可以直接扩展到矩阵构建——只需添加上面显示的 strategy.matrix 代码块即可。

Free tier

免费层级

ZeroDrop’s free tier includes unlimited inboxes, the full SDK, and the GitHub Action — no signup required, no credit card, no paywall on the API. ZeroDrop 的免费层级包含无限收件箱、完整的 SDK 和 GitHub Action——无需注册、无需信用卡、API 无付费墙。

For teams who need custom domains, 7-day retention, and shared API keys: 对于需要自定义域名、7 天保留期和共享 API 密钥的团队:

zerodrop.dev