Skip to content

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.

OperationLimitApplies to
Create captures10 / minPOST /api/v1/captures
Create diffs10 / minPOST /api/v1/diffs
Upload snapshots300 / minPOST /api/v1/snapshots
Create sessions20 / minPOST /api/v1/sessions
Upload sites5 / minPOST /api/v1/sites
Read endpoints100 / minGET on baselines, suites, overview

All limits are per API key. Different API keys have independent counters, even within the same project.

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:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining before the next reset
X-RateLimit-ResetUnix timestamp (seconds) when the window resets
Retry-AfterSeconds to wait before retrying (only on 429 responses)

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-After header, 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,
});
StatusRetried?Reason
429YesRate limited — wait and retry
500YesTransient server error
502YesBad gateway (Lambda cold start, etc.)
503YesService unavailable
400NoBad request — fix the input
401NoUnauthorized — check your API key
403NoForbidden
404NoNot found
409NoConflict

Network errors (connection reset, DNS failure) are also retried.

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 /snapshots within the same window.
  • Separate API keys have independent limits. Create per-workflow keys in Project Settings if you need higher effective throughput.

For large test suites (100+ screenshots), prefer fleet capture over manual upload:

WorkflowAPI calls for 500 screenshotsRate limit risk
pxdiff capture / pxdiff ladle1 POST + polling GETsNone
pxdiff upload (manual PNGs)500 POST /snapshotsModerate
Playwright/Vitest plugin500 POST /snapshotsModerate

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.

If you’re making direct API calls without the SDK:

Terminal window
# Check remaining quota from response headers
curl -si -H "X-API-Key: $PXDIFF_API_KEY" https://pxdiff.com/api/v1/baselines?suite=my-suite \
| grep -i x-ratelimit

When 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
}