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.
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-parallelon Free so concurrent creates stay under the IP budget - Put
OTPBOX_API_KEYin Actions secrets when shards × runs would exceed 10/hr - Log the disposable
emailon 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
- How to Test Email OTP Verification in CI
- Ultimate Guide to Email OTP Testing with Playwright
- CI Email OTP Checklist: 10 Steps That Kill Flaky Verification Tests
- 7 Mistakes That Make Email OTP Tests Flake in CI
- Should You Mock the Mailer in Email OTP E2E Tests?
Resources
- OTPBox — disposable inboxes for CI
- OTPBox API — creates,
/latest, Free vs Pro limits - Pricing — Pro API key for matrix headroom
- GitHub Actions matrix —
strategy.matrixandmax-parallel
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.