Rate limits, retries, and backoff
Bluestone PIM uses throttling to keep the Management API responsive and fair for all customers. Integrations should expect rate limits, handle 429 responses, and use retry logic with exponential backoff and jitter.
Use this guide when building batch jobs, scheduled syncs, AI agents, or other automation that can make many API requests in a short time.
Rate limits at a glance
| Property | Value |
|---|---|
| Standard steady rate | 10 requests per second |
| Burst allowance | Up to +10 requests per second (about 20 RPS peak for short spikes) |
| Scope | All clients combined per customer organization |
| Response when exceeded | HTTP 429 Too Many Requests |
| Rate-limit headers | Not exposed for the per-second throttle (see Response headers) |
| Higher limits | Available by contract agreement |
The standard 10 requests-per-second limit is a per-request rate, not a cap on how much data you can move. Bulk endpoints change up to 100 products per request, so one request inside the rate budget can affect hundreds of records. See Reduce rate-limit pressure.
How limits are applied
The standard Management API rate limit is 10 requests per second, applied across all clients combined within a customer organization. The exact limit is set in the contract agreement with Bluestone PIM, and customers that need higher sustained throughput should contact Bluestone PIM to review the contracted limit.
Ten requests per second is the request rate, not the work rate. Because bulk endpoints mutate up to 100 products per request (for example, Add attribute values in given set of products) and cursor reads return up to 100 items per request, a single request within the 10 RPS budget can read or change hundreds of records. Effective data throughput is therefore much higher than the raw request rate suggests when integrations use the bulk, cursor, and async patterns described in Reduce rate-limit pressure.
The Management API rate limit applies to all API clients combined within a customer organization. In practical terms, requests from multiple applications, client IDs, services, or access tokens can share the same organization-level allowance. Do not assume that creating more OAuth clients or access tokens increases total throughput.
| Question | Behavior |
|---|---|
| Scope | Shared across all clients within the customer organization |
| Client or token isolation | The organization-level allowance is shared; separate clients or access tokens do not increase total throughput |
| Contract variation | Exact limits are stipulated in the Bluestone PIM contract agreement |
| Endpoint variation | The contracted Management API limit is the integration planning baseline |
| Environment variation | Use the contracted production limit as the planning baseline; coordinate load testing in the test environment with Bluestone PIM |
| Customer tier variation | Managed through the customer contract agreement |
Standard throttle response
When a client exceeds the allowed rate, Bluestone PIM returns HTTP 429 Too Many Requests.
Clients should treat 429 as a retryable response. Retry the request after a delay, and increase that delay if additional 429 responses occur.
Response headers
Bluestone PIM rate limiting is evaluated per second, with a short burst allowance above the contracted steady rate. Because the throttle window is so short, fixed-window quota headers are less useful than they are for minute, hour, or day based limits.
For that reason, Bluestone PIM does not expose Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, or X-RateLimit-Reset for the Management API per-second throttle.
There are two practical reasons for this:
X-RateLimit-Resetwould usually point to the next second, so it would add little value compared with retrying after a short client-side delay.X-RateLimit-Remainingcan become stale immediately because the limit is shared by all clients in the same customer organization, not only the request currently being handled.
The supported integration pattern is:
- Know the contracted organization-level RPS limit.
- Apply client-side request pacing below that limit.
- Retry
429, transient5xx, and network failures with exponential backoff and jitter. - Reduce request volume by using bulk operations, cursor pagination, and async task status polling patterns.
Use a default backoff delay such as 1 second for the first retry, then increase the delay if the client continues to receive 429 responses.
Burst behavior
The burst limit allows short bursts above the steady rate, up to 10 additional requests per second beyond the contracted steady rate.
With the standard 10 requests-per-second rate, a client can therefore make up to 20 requests in a single second after an idle second:
| Previous second | Requests available in the next second |
|---|---|
| 0 requests | Up to 20 requests |
| 9 requests | Up to 11 requests |
| 10 requests | Up to 10 requests |
Do not design integrations to rely on the burst limit for sustained throughput. Use it only as a buffer for short spikes.
Retry strategy
Use exponential backoff with jitter for 429, transient 5xx, and network errors.
Recommended defaults:
| Setting | Recommendation |
|---|---|
| Retryable statuses | 429, 500, 502, 503, 504 |
| First retry delay | 1 second |
| Backoff multiplier | 2 |
| Jitter | Add random delay between 0 and 250 milliseconds, or use full jitter for high-concurrency clients |
| Maximum retries | 5 attempts for ordinary requests; fewer for user-facing calls |
| Maximum delay | Cap at 30 seconds unless your workflow can tolerate longer delays |
Retry only when the operation is safe to repeat. Read requests are usually safe. For write requests, use client-side deduplication and job tracking so a retry does not apply the same change twice.
Reduce rate-limit pressure
Prefer API patterns that reduce the number of requests your integration needs to make.
| Pattern | Use it for | Reference |
|---|---|---|
| Cursor pagination | Reading large product result sets without page-number drift | Get products using cursor with details from given views |
| Bulk operations | Updating many products or attribute values in fewer calls | Add attribute values in given set of products |
| Async task status polling | Tracking long-running jobs without repeating the original operation | Get async task status |
| Metadata async task status | Tracking metadata async tasks | Get task status |
When polling async task status endpoints, start with a short delay, then back off while the task remains in progress. Avoid tight loops such as polling every 100 milliseconds.
Python example
This example retries 429, transient 5xx, and network failures with exponential backoff and jitter.
Set BLUESTONE_API_BASE and BLUESTONE_AUTH_BASE from the Environments page.
import random
import time
from typing import Any
import requests
BLUESTONE_AUTH_BASE = "https://idp.test.bluestonepim.com/op/token"
BLUESTONE_API_BASE = "https://api.test.bluestonepim.com"
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
RETRY_STATUSES = {429, 500, 502, 503, 504}
MAX_RETRIES = 5
TIMEOUT_SECONDS = 20
def get_token() -> str:
response = requests.post(
BLUESTONE_AUTH_BASE,
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
timeout=TIMEOUT_SECONDS,
)
response.raise_for_status()
return response.json()["access_token"]
def retry_delay_seconds(attempt: int) -> float:
base_delay = 2 ** attempt
jitter = random.uniform(0, 0.25)
return min(base_delay + jitter, 30)
def request_api(method: str, path: str, token: str, json_body: dict[str, Any] | None = None) -> Any:
url = f"{BLUESTONE_API_BASE}/{path.lstrip('/')}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"context-fallback": "true",
}
for attempt in range(MAX_RETRIES):
try:
response = requests.request(
method,
url,
headers=headers,
json=json_body,
timeout=TIMEOUT_SECONDS,
)
if response.status_code not in RETRY_STATUSES:
response.raise_for_status()
return response.json() if response.content else None
if attempt == MAX_RETRIES - 1:
response.raise_for_status()
time.sleep(retry_delay_seconds(attempt))
except (requests.ConnectionError, requests.Timeout):
if attempt == MAX_RETRIES - 1:
raise
time.sleep(retry_delay_seconds(attempt))
raise RuntimeError("Request failed after retries")
token = get_token()
catalogs = request_api("GET", "/pim/catalogs", token)
print(catalogs)Node/TypeScript example
This example uses the built-in fetch API available in current Node.js versions.
Set BLUESTONE_API_BASE and BLUESTONE_AUTH_BASE from the Environments page.
const BLUESTONE_AUTH_BASE = "https://idp.test.bluestonepim.com/op/token";
const BLUESTONE_API_BASE = "https://api.test.bluestonepim.com";
const CLIENT_ID = "YOUR_CLIENT_ID";
const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
const RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);
const MAX_RETRIES = 5;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function retryDelayMs(attempt: number): number {
const baseDelay = 2 ** attempt * 1000;
const jitter = Math.floor(Math.random() * 250);
return Math.min(baseDelay + jitter, 30000);
}
async function getToken(): Promise<string> {
const body = new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
});
const response = await fetch(BLUESTONE_AUTH_BASE, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
const json = await response.json();
return json.access_token;
}
async function requestApi<T>(method: string, path: string, token: string, body?: unknown): Promise<T> {
const url = `${BLUESTONE_API_BASE}/${path.replace(/^\/+/, "")}`;
for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
try {
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"context-fallback": "true",
},
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!RETRY_STATUSES.has(response.status)) {
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return response.status === 204 ? (undefined as T) : ((await response.json()) as T);
}
if (attempt === MAX_RETRIES - 1) {
throw new Error(`API request failed after retries: ${response.status}`);
}
await sleep(retryDelayMs(attempt));
} catch (error) {
if (!(error instanceof TypeError)) {
throw error;
}
if (attempt === MAX_RETRIES - 1) {
throw error;
}
await sleep(retryDelayMs(attempt));
}
}
throw new Error("API request failed after retries");
}
const token = await getToken();
const catalogs = await requestApi<unknown>("GET", "/pim/catalogs", token);
console.log(catalogs);cURL example
This shell example retries 429, transient 5xx, and network failures with exponential backoff and jitter using curl and bash.
Set BLUESTONE_API_BASE and BLUESTONE_AUTH_BASE from the Environments page.
#!/usr/bin/env bash
set -euo pipefail
BLUESTONE_AUTH_BASE="https://idp.test.bluestonepim.com/op/token"
BLUESTONE_API_BASE="https://api.test.bluestonepim.com"
CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_SECRET="YOUR_CLIENT_SECRET"
MAX_RETRIES=5
# Retryable statuses: 429, 500, 502, 503, 504
is_retryable() {
case "$1" in
429|500|502|503|504) return 0 ;;
*) return 1 ;;
esac
}
# Exponential backoff with jitter, capped at 30 seconds.
backoff_sleep() {
local attempt="$1"
local base=$((2 ** attempt))
local jitter_ms=$((RANDOM % 250))
local delay
delay=$(awk -v b="$base" -v j="$jitter_ms" 'BEGIN { d = b + j/1000; if (d > 30) d = 30; print d }')
sleep "$delay"
}
get_token() {
curl -fsS -X POST "$BLUESTONE_AUTH_BASE" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p'
}
# Usage: request_api GET /pim/catalogs
request_api() {
local method="$1" path="$2"
local url="${BLUESTONE_API_BASE}/${path#/}"
local attempt status body
for ((attempt = 0; attempt < MAX_RETRIES; attempt++)); do
body=$(mktemp)
status=$(curl -sS -o "$body" -w "%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-H "context-fallback: true" || echo "000")
if [[ "$status" == "000" ]] || is_retryable "$status"; then
if (( attempt == MAX_RETRIES - 1 )); then
echo "Request failed after retries (last status: ${status})" >&2
cat "$body" >&2; rm -f "$body"; return 1
fi
rm -f "$body"
backoff_sleep "$attempt"
continue
fi
cat "$body"; rm -f "$body"
return 0
done
}
TOKEN="$(get_token)"
request_api GET /pim/catalogsUpdated 5 days ago
