← Back to Blog

GitHub Actions Matrix + OTPBox Create Quota: Avoiding 429s in Parallel OTP E2E

Parallel matrix jobs burn Free-tier inbox creates fast. Cap concurrency with max-parallel, create once per job, and use OTPBOX_API_KEY when you need 120 creates/hr.

testingotpci-cdgithub-actionshow-to
GitHub Actions Matrix + OTPBox Create Quota: Avoiding 429s in Parallel OTP E2E

A GitHub Actions matrix multiplies jobs — and each job that calls POST /api/inbox burns one Free-tier create. Hit 10 creates/hour per IP and you get 429. This how-to shows how to keep parallel OTP E2E green without inventing bypass codes.

Claims match the live OTPBox API, GitHub's matrix docs, and Pro-in-CI wiring. No invented competitor stats.

Why matrix jobs trip create quota

Matrix strategies expand one job definition into many runs (OS × browser × shard). That is great for coverage and terrible for a per-IP create ceiling if every shard opens a fresh disposable inbox at once.

OTPBox Free (from /docs):

Knob Free Pro ($9/mo)
Inbox creates 10/hr per IP 120/hr with API key
Inbox TTL 1h 6h
Poll /latest Unrestricted Unrestricted

Only POST /api/inbox counts. Polling does not. So the failure mode is almost always too many creates in the same hour, not too many polls.

GitHub-hosted runners use dynamic outbound IPs from shared pools (not a fixed address you can plan around). Self-hosted runners behind one NAT share one egress IP — so a 12-shard matrix can burn Free quota in a single wave. Treat Free as a laptop / light CI budget; treat matrix + parallel OTP as a Pro problem or a concurrency problem.

Live Free 429 body (from /docs):

{
  "error": "Free tier: 10 inbox creates/hr per IP. Need more for CI? See Pro → https://otpbox.app/pricing"
}

Rule 1: one create per job, never inside the poll loop

Create once, trigger the real send, then poll /latest until a code appears. Recreating the inbox on every retry is the fastest way to 429 yourself.

# Create once (optional Pro key)
AUTH=()
if [ -n "$OTPBOX_API_KEY" ]; then
  AUTH=(-H "Authorization: Bearer $OTPBOX_API_KEY")
fi
RESPONSE=$(curl -s -X POST https://otpbox.app/api/inbox "${AUTH[@]}")
TOKEN=$(echo "$RESPONSE" | jq -r '.token')
EMAIL=$(echo "$RESPONSE" | jq -r '.email')

# Trigger your app to send to $EMAIL …
# Then poll — unrestricted, no key required
for i in {1..20}; do
  OTP=$(curl -s "https://otpbox.app/api/inbox/$TOKEN/latest" | jq -r '.otp // empty')
  [ -n "$OTP" ] && [ "$OTP" != "null" ] && break
  sleep 3
done

Same idea in Playwright with expect.poll — see How Playwright expect.poll Beats Fixed Sleeps for Email OTP. Never call POST /api/inbox inside the poll callback.

Rule 2: cap matrix concurrency with max-parallel

GitHub lets you limit how many matrix jobs run at once with strategy.max-parallel. On Free, keep concurrent creates under the hourly budget you can afford (remember other workflows may share the same egress IP).

jobs:
  e2e-otp:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      max-parallel: 2   # two creates at a time — not twelve
      matrix:
        shard: [1, 2, 3, 4, 5, 6]
    steps:
      - uses: actions/checkout@v4
      - name: Create OTPBox inbox
        id: inbox
        env:
          OTPBOX_API_KEY: ${{ secrets.OTPBOX_API_KEY }}
        run: |
          AUTH=()
          if [ -n "$OTPBOX_API_KEY" ]; then
            AUTH=(-H "Authorization: Bearer $OTPBOX_API_KEY")
          fi
          RESPONSE=$(curl -s -X POST https://otpbox.app/api/inbox "${AUTH[@]}")
          echo "token=$(echo "$RESPONSE" | jq -r '.token')" >> "$GITHUB_OUTPUT"
          echo "email=$(echo "$RESPONSE" | jq -r '.email')" >> "$GITHUB_OUTPUT"
      # trigger signup with steps.inbox.outputs.email, then poll /latest …

max-parallel alone does not raise the Free ceiling — it stretches creates across wall-clock time so you stay under 10/hr on a shared IP. If you still need twelve shards in one hour, you need Pro.

Rule 3: use Pro when matrix needs headroom

Store the key as OTPBOX_API_KEY (repo or org secret). Send it only on create:

env:
  OTPBOX_API_KEY: ${{ secrets.OTPBOX_API_KEY }}
curl -s -X POST https://otpbox.app/api/inbox \
  -H "Authorization: Bearer $OTPBOX_API_KEY"
# or: -H "x-api-key: $OTPBOX_API_KEY"

Polls stay unauthenticated. Free still works without the secret for a single-job smoke; Pro is for 120 creates/hr and 6h TTL when matrix / long suites need it. Full copy: /docs → Using Pro in CI. Pricing: /pricing.

Do / don't checklist

Do

  • Create one inbox per matrix job (or per Playwright worker), then poll
  • Set max-parallel on Free so concurrent creates stay under the IP budget
  • Put OTPBOX_API_KEY in Actions secrets when shards × runs would exceed 10/hr
  • Log the disposable email on failure for provider traces
  • Prefer sharding that reuses a worker's inbox across related steps in the same job

Don't

  • Create inside retry/poll loops
  • Share one inbox across parallel jobs (cross-talk)
  • Assume GitHub-hosted runners give you a stable IP you can allowlist for Free quota planning
  • Treat polling volume as the rate-limit problem — it is not

Related reading

Resources


Matrix coverage and disposable inboxes work together when you treat creates as the scarce resource: cap concurrency on Free, or unlock Pro when parallel OTP E2E is part of every PR.