Rate Limits
Rate limits protect the pxdiff API from abuse and ensure fair access for all users. Limits are enforced per API key within a sliding 60-second window.
Limits by operation
Section titled “Limits by operation”| Operation | Limit | Applies to |
|---|---|---|
| Create captures | 10 / min | POST /api/v1/captures |
| Create diffs | 10 / min | POST /api/v1/diffs |
| Upload snapshots | 300 / min | POST /api/v1/snapshots |
| Create sessions | 20 / min | POST /api/v1/sessions |
| Upload sites | 5 / min | POST /api/v1/sites |
| Read endpoints | 100 / min | GET on baselines, suites, overview |
All limits are per API key. Different API keys have independent counters, even within the same project.
What happens when you hit a limit
Section titled “What happens when you hit a limit”The API returns a 429 status code with a Retry-After header:
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests" }}Every response includes rate limit headers so you can monitor your usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining before the next reset |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
Retry-After | Seconds to wait before retrying (only on 429 responses) |
Automatic retry
Section titled “Automatic retry”The SDK (@pxdiff/sdk v0.14+), CLI, Playwright plugin, and Vitest plugin automatically retry on 429 and transient server errors (500, 502, 503) with exponential backoff.
Default behavior:
- 3 retries (4 total attempts)
- Exponential backoff starting at 1 second, doubling each attempt
- Jitter (±25%) to prevent thundering herd from parallel CI jobs
- Retry-After respected — when the server sends a
Retry-Afterheader, it overrides the calculated backoff
You can configure retry behavior when creating a client:
import { PxdiffClient } from "@pxdiff/sdk";
const client = new PxdiffClient({ apiKey: process.env.PXDIFF_API_KEY, maxRetries: 5, // default: 3 retryBaseDelayMs: 2000, // default: 1000});To disable retry entirely:
const client = new PxdiffClient({ apiKey: process.env.PXDIFF_API_KEY, maxRetries: 0,});Which errors are retried
Section titled “Which errors are retried”| Status | Retried? | Reason |
|---|---|---|
429 | Yes | Rate limited — wait and retry |
500 | Yes | Transient server error |
502 | Yes | Bad gateway (Lambda cold start, etc.) |
503 | Yes | Service unavailable |
400 | No | Bad request — fix the input |
401 | No | Unauthorized — check your API key |
403 | No | Forbidden |
404 | No | Not found |
409 | No | Conflict |
Network errors (connection reset, DNS failure) are also retried.
CI concurrency
Section titled “CI concurrency”Rate limits are per API key, not per IP address. This means:
- Parallel CI jobs sharing the same API key compound against the same limits. If you run 5 parallel jobs each uploading 100 snapshots, that’s 500
POST /snapshotswithin the same window. - Separate API keys have independent limits. Create per-workflow keys in Project Settings if you need higher effective throughput.
Choosing the right workflow
Section titled “Choosing the right workflow”For large test suites (100+ screenshots), prefer fleet capture over manual upload:
| Workflow | API calls for 500 screenshots | Rate limit risk |
|---|---|---|
pxdiff capture / pxdiff ladle | 1 POST + polling GETs | None |
pxdiff upload (manual PNGs) | 500 POST /snapshots | Moderate |
| Playwright/Vitest plugin | 500 POST /snapshots | Moderate |
Fleet capture (pxdiff capture, pxdiff ladle, Storybook Action) submits all targets in a single API call — screenshots are taken server-side. This is the most efficient path for large suites.
The Playwright and Vitest plugins upload screenshots individually as tests complete, which works well for typical component test suites (under 300 screenshots). For very large suites, the automatic retry handles any 429s transparently.
Handling rate limits manually
Section titled “Handling rate limits manually”If you’re making direct API calls without the SDK:
# Check remaining quota from response headerscurl -si -H "X-API-Key: $PXDIFF_API_KEY" https://pxdiff.com/api/v1/baselines?suite=my-suite \ | grep -i x-ratelimitWhen you receive a 429, wait for the number of seconds in the Retry-After header before retrying:
const res = await fetch(url, { headers: { "X-API-Key": apiKey } });
if (res.status === 429) { const retryAfter = parseInt(res.headers.get("Retry-After") ?? "60", 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); // Retry the request}