Rate Limits
TL;DR
Every client IP address may send up to 1,000 requests per minute to the GraphQL endpoints. Above that, requests are rejected with HTTP 429 and a GraphQL-style JSON error until the rate drops below the limit again. Do not retry rejected requests immediately: back off, spread out your traffic, and cache what you can.
The Catalog API and the Purple Publish API are shared by all Purple customers. A rate limit protects the platform from runaway integrations, misconfigured retry loops and abusive traffic, so that one client cannot degrade the service for everyone else. The limit is generous for normal app, website and integration traffic. Well-behaved clients will never see it.
What is limited
Limit | 1,000 requests per minute |
|---|---|
Counted per | Client IP address (the public IP the request arrives from) |
Window | Rolling 1-minute window |
Applies to | All HTTP requests to a /graphql path on the hosts below |
On exceed | HTTP 429 Too Many Requests with a JSON error body |
Affected endpoints
- https://catalog.purplemanager.com/graphql
- https://api.purplepublish.com/graphql
What counts as a request
Every HTTP request that reaches one of the endpoints above counts towards the limit, independent of the query it carries:
- GraphQL requests, including introspection and queries sent from the GraphiQL editor.
- Requests that are answered from the CDN cache.
- CORS preflight (OPTIONS) requests sent by browsers.
- Requests that fail, for example with a validation error or an invalid API key.
The number of fields or operations inside a request does not matter. One request containing three queries counts once. Three separate requests count three times.
What is not limited
- Downloading content resources such as images, PDFs or issue packages from the CDN.
- GraphQL subscriptions over WebSocket (/subscriptions).
The 429 response
When a client IP exceeds the limit, further requests from that IP are rejected. The response uses HTTP status 429 Too Many Requests and carries a body in the same shape as a GraphQL error response, so existing GraphQL error handling can pick it up:
Rejections continue until the request rate of that IP falls below 1,000 requests per minute again, which usually takes well under a minute once the client slows down. The response currently does not include a Retry-After header.
Production rollout
Enforcement on production begins on [DATE – to be announced]. Until then, requests above the limit are counted but not rejected.
How your client should react
Check for HTTP status 429 or for an error with extensions.code equal to RATE_LIMITED, then:
- Stop sending immediately. Do not fire the same request again in a tight loop. Every retry counts and prolongs the block.
- Back off exponentially with jitter. Wait about 1 second before the first retry, then 2, 4, 8 seconds and so on, each with a random offset. Stop after a small number of attempts and surface the error.
- Treat it as a signal to slow down globally. When one request is rejected, all concurrent requests from the same IP are affected. Pause your request queue instead of letting every worker retry on its own.
- Handle other failures the same way. Apply the same backoff to timeouts and 5xx responses. Aggressive retries during an incident make the incident worse for everyone, including you.
A minimal example in JavaScript:
Staying well below the limit
1,000 requests per minute is roughly 16 requests per second from a single IP. Typical apps and websites stay far below that. Server-side integrations are the ones that need attention.
Combine queries
GraphQL lets you request several connections in one request. A page that needs a post, its related posts and the menu should send one request, not three.
Paginate deliberately
Pages are limited to 200 entries. Fetching what you need with first: 200 uses far fewer requests than walking through the result set 20 entries at a time.
Cache on your side
Server-side rendering is the most common source of excessive traffic. Cache rendered pages or the underlying query results, and use the content's lastModified timestamps to decide when to refresh. A cache miss should trigger one Catalog API request, not one per component.
Watch for crawlers on your own site
Bots and crawlers visiting your website trigger your server-side rendering, which in turn calls the Catalog API. A crawler working through thousands of long-tail URLs can multiply your request rate overnight. Serve them from your cache, and make sure redirect chains and link loops on your side cannot cause the same page to be rendered repeatedly.
Spread out bulk work
Full re-indexing, sitemap generation and similar jobs should run at a steady, throttled pace rather than as a burst. If a job needs 50,000 requests, running it over an hour instead of five minutes keeps it well under the limit.
Mind shared egress IPs
The limit is per public IP. If your website, your backend services and your import jobs all leave your network through the same NAT gateway or proxy, they share one budget of 1,000 requests per minute.
Send a descriptive User-Agent
Include your product and version, for example acme-website/2.3.1. It lets us attribute traffic to your integration and contact you about unusual patterns before they become a problem.
Need a higher limit?
Talk to us first
If your integration legitimately needs more than 1,000 requests per minute from a single IP, contact Purple support before you go live. Please include the affected endpoint, the public IP addresses you send from, your User-Agent, and the peak request rate you expect. We will review the pattern with you and, where appropriate, look for ways to reduce the request volume or adjust the limit.