Rate limits and limits
Rate limits, query complexity and request size limits.
The delivery API has a few limits that keep it fast for everyone. Normal websites never get near them, especially ones that cache.
| Limit | Value | When you go over |
|---|---|---|
| Requests | 25 a second per IP address | 429 with Rate limit exceeded. |
| Query complexity | 10,000 per request | GRAPHQL_VALIDATION_FAILED |
| Page size | 100 documents per list | Larger limit values return 100 |
| Request body | 256 KB | 413 with an empty body |
Request rate#
Each IP address can send 25 requests in any one-second window. Every response has an X-RateLimit-Remaining header with how many requests you have left in the current window. Requests that were rejected count too, so retrying in a tight loop keeps you limited.
HTTP/1.1 429 Too Many RequestsX-RateLimit-Remaining: 0Content-Type: text/plainRate limit exceeded.
Retry with backoff#
On a 429, wait and try again, waiting longer each time:
async function fetchWithRetry(body, attempts = 3) {for (let attempt = 1; ; attempt += 1) {const response = await fetch('https://content.pmkin.io/graphql', {body: JSON.stringify(body),headers: {Authorization: `Bearer ${process.env.PMKIN_API_KEY}`,'Content-Type': 'application/json'},method: 'POST',signal: AbortSignal.timeout(10_000)})if (response.status !== 429 || attempt === attempts) {return response}// Wait 1, 2, 4 seconds… plus some jitter.const delay = 1000 * 2 ** (attempt - 1) + Math.random() * 250await new Promise((resolve) => {setTimeout(resolve, delay)})}}
import osimport randomimport timeimport requestsdef fetch_with_retry(body, attempts=3):for attempt in range(1, attempts + 1):response = requests.post('https://content.pmkin.io/graphql',headers={'Authorization': f"Bearer {os.environ['PMKIN_API_KEY']}"},json=body,timeout=10,)if response.status_code != 429 or attempt == attempts:return responsetime.sleep(2 ** (attempt - 1) + random.random() / 4)
Cache responses#
The best way to stay under the limit is to not ask twice. Content changes when someone publishes, not on every page view, so:
- Build static pages at deploy time, or cache rendered pages for a few minutes.
- Fetch everything a page needs in one request: GraphQL lets you combine queries.
- During a static build, run requests a few at a time instead of all at once.
Query complexity#
Before it runs a query, the API adds up what it costs. A query that costs more than 10,000 is rejected with GRAPHQL_VALIDATION_FAILED.
- Most fields cost 1.
htmlcosts 10, because rendering it is the expensive part. - An object field like
categoryorcoverImagecosts 1 plus its fields. - A list costs its
limit, 100 by default, times the cost of one document.
query Blog {documents(limit: 100) { # 100 ×title # 1slug # + 1html # + 10} # = 1,200}
Lists rarely need the full text. Ask for excerpt on index pages and fetch html with documentBySlug on the document’s own page.
Request size#
Request bodies can be up to 256 KB, far more than any query needs. If you hit it, you’re probably inlining data into the query: pass it as variables instead.