Rate limits

The published limits per endpoint, and an honest account of how the limiter actually behaves in production.

Three public endpoints are rate limited, by IP address, on a sliding one-minute window. This page states the numbers and then explains what they actually mean in production, which is less exact than a limits table usually implies.

The limits#

ParameterTypeDescription
GET /r/<code>60 / minute / IPThe tracked link redirect. Generous, because a legitimate burst is normal — a creator's audience clicking at once, or an office behind one NAT address.
POST /api/conversion30 / minute / IPConversions are low-frequency by nature, so a tight cap costs nothing. See Conversion endpoint.
POST /report/<token>/pdf10 / minute / IPThe tightest, because each call costs money at a third-party renderer and takes seconds.

Everything else is unlimited: the Shopify webhooks (HMAC-authenticated, and throttling Shopify's retries would only delay real orders), the CSV export (session-authenticated), and the report page itself.

The 429#

POST /api/conversion, over the limit
HTTP/1.1 429
Retry-After: 43
Content-Type: application/json

{ "ok": false, "error": "rate_limited" }

Retry-After is in seconds, and is calculated from when the oldest request in your current window expires — so waiting exactly that long is enough, rather than a guess. The tracked-link redirect returns the same header with a plain-text body.

What the limiter actually is#

Warning.the limiter is an in-memory sliding window held per server instance. It is deliberately best-effort, and the numbers above are not enforced globally.

Three consequences worth knowing before you design around it:

  • The real limit is higher than the number. Counters live in the memory of whichever instance served the request. With several instances running, the same IP can be allowed roughly the limit per instance before anything is rejected.
  • It resets on cold start. A new instance begins with an empty map, so a quiet period followed by a scale-up effectively clears the count.
  • Counters are dropped under memory pressure. Past about 5,000 tracked keys, expired entries are swept. A key swept mid-window starts from zero.

This is a blunt instrument against casual abuse of the public endpoints, not a quota system. Treat the published numbers as the point at which you will start seeing 429s, not as a ceiling you can safely run up to. A distributed store is the upgrade path if this ever needs to be exact.

How your IP is determined#

The first entry in X-Forwarded-For, falling back to X-Real-IP.

Warning.when neither header is present the request is bucketed under the literal string unknown. Every such caller therefore shares one counter, and can exhaust the limit for all the others. In normal operation the hosting platform always sets X-Forwarded-For, so this affects direct-to-origin traffic and server-side calls made without it.

Since X-Forwarded-For is caller-supplied, it is also spoofable by anyone talking directly to the origin. This is another reason the limiter is a speed bump rather than a security control — the real protections are HMAC verification on the webhooks and the signed, short-lived link on PDF rendering.

Handling 429 in your integration#

The pixel does not handle it. The request is fire-and-forget, the promise rejection is swallowed, and a rate-limited conversion is simply lost with no retry and no console warning. If you are calling /api/conversion from your own server instead, read Retry-After and retry once — and send an external_order_id, so the retry is deduplicated rather than counted twice.