From shopify-pack
Manages Shopify REST leaky bucket and GraphQL query cost rate limits to handle 429s, implement retries, and optimize throughput.
How this skill is triggered — by the user, by Claude, or both
Slash command
/shopify-pack:shopify-rate-limitsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Shopify uses two distinct rate limiting systems: leaky bucket for REST and calculated query cost for GraphQL. This skill covers both with real header values and response shapes.
Shopify uses two distinct rate limiting systems: leaky bucket for REST and calculated query cost for GraphQL. This skill covers both with real header values and response shapes.
@shopify/shopify-api libraryREST Admin API — Leaky Bucket:
| Plan | Bucket Size | Leak Rate |
|---|---|---|
| Standard | 40 requests | 2/second |
| Shopify Plus | 80 requests | 4/second |
The X-Shopify-Shop-Api-Call-Limit header shows your bucket state:
X-Shopify-Shop-Api-Call-Limit: 32/40
Means: 32 of 40 slots used. When full, you get HTTP 429 with Retry-After header.
GraphQL Admin API — Calculated Query Cost:
| Plan | Max Available | Restore Rate |
|---|---|---|
| Standard | 1,000 points | 50 points/second |
| Shopify Plus | 2,000 points | 100 points/second |
Every GraphQL response includes cost info in extensions:
{
"extensions": {
"cost": {
"requestedQueryCost": 252,
"actualQueryCost": 12,
"throttleStatus": {
"maximumAvailable": 1000.0,
"currentlyAvailable": 988.0,
"restoreRate": 50.0
}
}
}
}
Key insight: requestedQueryCost is the worst case estimate. actualQueryCost is the real cost (often much lower). When currentlyAvailable drops to 0, you get THROTTLED.
interface ShopifyThrottleStatus {
maximumAvailable: number;
currentlyAvailable: number;
restoreRate: number;
}
class ShopifyRateLimiter {
private available: number;
private restoreRate: number;
private lastUpdate: number;
constructor(maxAvailable = 1000, restoreRate = 50) {
this.available = maxAvailable;
this.restoreRate = restoreRate;
this.lastUpdate = Date.now();
}
updateFromResponse(throttleStatus: ShopifyThrottleStatus): void {
this.available = throttleStatus.currentlyAvailable;
this.restoreRate = throttleStatus.restoreRate;
this.lastUpdate = Date.now();
}
async waitIfNeeded(estimatedCost: number): Promise<void> {
// Estimate current available based on restore rate
const elapsed = (Date.now() - this.lastUpdate) / 1000;
const estimated = Math.min(
this.available + elapsed * this.restoreRate,
1000
);
if (estimated < estimatedCost) {
const waitSeconds = (estimatedCost - estimated) / this.restoreRate;
console.log(`Rate limit: waiting ${waitSeconds.toFixed(1)}s for ${estimatedCost} points`);
await new Promise((r) => setTimeout(r, waitSeconds * 1000));
}
}
}
// Usage
const limiter = new ShopifyRateLimiter();
async function rateLimitedQuery(client: any, query: string, variables?: any) {
await limiter.waitIfNeeded(100); // estimate cost
const response = await client.request(query, { variables });
// Update limiter from actual response
if (response.extensions?.cost?.throttleStatus) {
limiter.updateFromResponse(response.extensions.cost.throttleStatus);
}
return response;
}
async function withShopifyRetry<T>(
operation: () => Promise<T>,
maxRetries = 5
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error: any) {
const isThrottled =
error.response?.code === 429 ||
error.body?.errors?.[0]?.extensions?.code === "THROTTLED";
if (!isThrottled || attempt === maxRetries) throw error;
// Use Retry-After header if available (REST), otherwise calculate
const retryAfter = error.response?.headers?.["retry-after"];
const delay = retryAfter
? parseFloat(retryAfter) * 1000
: Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
console.warn(
`Shopify throttled (attempt ${attempt + 1}/${maxRetries}). ` +
`Retrying in ${(delay / 1000).toFixed(1)}s`
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}
// EXPENSIVE query — requests all fields, high cost
const EXPENSIVE = `{
products(first: 250) {
edges {
node {
id title description
variants(first: 100) {
edges {
node {
id title price sku inventoryQuantity
metafields(first: 10) {
edges { node { key value } }
}
}
}
}
images(first: 20) {
edges { node { url altText } }
}
}
}
}
}`;
// requestedQueryCost: ~5,502 (may THROTTLE immediately)
// OPTIMIZED query — only needed fields, lower page sizes
const OPTIMIZED = `{
products(first: 50) {
edges {
node {
id
title
variants(first: 10) {
edges {
node { id price sku }
}
}
}
}
pageInfo { hasNextPage endCursor }
}
}`;
// requestedQueryCost: ~112 (safe, leaves room for other queries)
# Add this header to see cost breakdown per field
curl -X POST "https://store.myshopify.com/admin/api/2024-10/graphql.json" \
-H "X-Shopify-Access-Token: $TOKEN" \
-H "Content-Type: application/json" \
-H "Shopify-GraphQL-Cost-Debug: 1" \
-d '{"query": "{ products(first: 10) { edges { node { id title } } } }"}' \
| jq '.extensions.cost'
| Scenario | REST Indicator | GraphQL Indicator |
|---|---|---|
| Approaching limit | X-Shopify-Shop-Api-Call-Limit: 38/40 | currentlyAvailable < 100 |
| At limit | HTTP 429 + Retry-After: 2.0 | errors[0].extensions.code: "THROTTLED" |
| Recovering | Wait for Retry-After seconds | Wait for restoreRate to refill |
import PQueue from "p-queue";
// For bulk operations, use Shopify's bulk query API instead
const BULK_QUERY = `
mutation bulkOperationRunQuery($query: String!) {
bulkOperationRunQuery(query: $query) {
bulkOperation {
id
status
url
}
userErrors { field message }
}
}
`;
// Bulk queries bypass rate limits for large data exports
await client.request(BULK_QUERY, {
variables: {
query: `{
products {
edges {
node {
id
title
variants { edges { node { id sku price } } }
}
}
}
}`,
},
});
For security configuration, see shopify-security-basics.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin shopify-packOptimizes Shopify GraphQL API performance through query cost reduction, bulk operations, caching strategies, and Storefront API for high-traffic storefronts.
Use Shopify GraphQL Admin API for server CRUD on products/orders and Storefront API for client queries on products/cart, with versioning, rate limits, bulk ops, pagination.
Provides expert patterns for Shopify app development including Remix/React Router setup, embedded apps with App Bridge, webhook handling, GraphQL Admin API, Polaris components, billing, and app extensions.