Rate Limiting
Default Limits
The default rate limit is 100 requests per 20 seconds per authenticated user. Requests that use a high offset parameter (above 30,000) are subject to a stricter limit of 75 requests per 20 seconds.
These are the default limits. Individual endpoints may enforce their own limits that are lower or higher than the defaults. Rate limits can also be adjusted dynamically at any time and without prior notice. Your application should never hard-code rate limit values.
Rate Limit Headers
Every API response includes headers that describe the current rate limit state for your user. Always rely on these headers—not assumed values—when deciding how to pace your requests.
| Header | Description |
|---|---|
X-PCO-API-Request-Rate-Limit |
The maximum number of requests allowed in the current period. |
X-PCO-API-Request-Rate-Period |
The length of the current rate limit window (e.g., 20 seconds). |
X-PCO-API-Request-Rate-Count |
How many requests you have made so far in the current period. |
Retry-After |
Included on 429 responses. The number of seconds to wait before retrying. |
Handling a 429 Response
When you exceed the rate limit, the API returns HTTP status 429 Too Many Requests.
Sample response headers (notice the request count exceeds the limit):
$ curl -sS -D /dev/stderr -H 'Authorization: ...' https://api.planningcenteronline.com/people/v2/me
HTTP/2 429
content-type: application/json
content-length: 122
x-pco-api-request-rate-count: 118
x-pco-api-request-rate-limit: 100
x-pco-api-request-rate-period: 20 seconds
retry-after: 19
Sample response body:
{
"errors": [
{
"code": "429",
"detail": "Rate limit exceeded: 118 of 100 requests per 20 seconds"
}
]
}
Example: A Rate-Limit-Aware Client
The following JavaScript example reads the rate limit headers on every response and automatically waits when a 429 is returned. This approach adapts to whatever limits the API returns, so your application stays well-behaved even if limits change.
Note: This example automatically retries on 429, so it is best suited for idempotent requests (e.g., GET). If you use it for POST or other non-idempotent methods, ensure the request is safe to replay.
const MAX_RETRIES = 5;
async function pcoFetch(url, options = {}, retries = 0) {
const response = await fetch(url, options);
// Read the current rate limit state from the response headers.
const limit = parseInt(response.headers.get("X-PCO-API-Request-Rate-Limit"), 10);
const count = parseInt(response.headers.get("X-PCO-API-Request-Rate-Count"), 10);
const retryAfter = parseInt(response.headers.get("Retry-After"), 10);
if (response.status === 429) {
if (retries >= MAX_RETRIES) {
throw new Error("Rate limit: max retries exceeded");
}
// Wait for the amount of time the API tells us to, then retry.
const delay = (retryAfter || 1) * 1000;
console.warn(`Rate limited. Retrying after ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
return pcoFetch(url, options, retries + 1);
}
// Optionally, proactively slow down when approaching the limit.
// Adjust the threshold (0.8) and delay (1000ms) to suit your needs.
if (count >= limit * 0.8) {
console.log(`Approaching rate limit (${count}/${limit}). Slowing down.`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return response;
}