Shopify API Rate Limiting: Best Practices for Bulk Operations
How to handle Shopify's API rate limits when syncing products, inventory, and orders in bulk without getting throttled.
The Rate Limit Problem
Every Shopify app hits rate limits eventually. Whether you're syncing inventory across channels, importing bulk orders, or updating thousands of products, Shopify's API has strict limits that will slow you down — or shut you out — if you're not careful.
Understanding Shopify's Rate Limits
Shopify uses a leaky bucket algorithm for REST API rate limiting:
- Bucket size: 40 requests
- Leak rate: 2 requests per second
- Overage: Requests beyond the bucket size get a 429 response
For GraphQL, it's based on query cost:
- Bucket size: 1,000 points
- Restore rate: 50 points per second
- Each query has a calculated cost based on complexity
Strategy 1: Respect the Headers
Every Shopify REST API response includes rate limit headers:
async function shopifyRequest(endpoint, options = {}) {
const response = await fetch(
`https://${STORE}.myshopify.com/admin/api/2024-01/${endpoint}`,
{
...options,
headers: {
'X-Shopify-Access-Token': TOKEN,
'Content-Type': 'application/json',
...options.headers
}
}
);
// Check rate limit status
const rateLimitHeader = response.headers.get('X-Shopify-Shop-Api-Call-Limit');
const [used, total] = rateLimitHeader.split('/').map(Number);
if (used >= total - 5) {
// Getting close to limit, slow down
await sleep(1000);
}
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 2;
await sleep(retryAfter * 1000);
return shopifyRequest(endpoint, options); // Retry
}
return response.json();
}
Strategy 2: Use GraphQL for Bulk Operations
GraphQL is more efficient than REST for bulk operations because you can fetch or mutate multiple resources in a single request:
const BULK_PRODUCT_QUERY = `
mutation bulkOperationRunQuery {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
title
variants {
edges {
node {
id
sku
inventoryQuantity
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
`;
Bulk operations run asynchronously and return a JSONL file with all results. This is dramatically more efficient than paginating through REST endpoints.
Strategy 3: Queue and Throttle
For real-time syncs that need to make many API calls, use a queue with rate limiting:
const Bottleneck = require('bottleneck');
const limiter = new Bottleneck({
maxConcurrent: 2,
minTime: 500 // 2 requests per second max
});
async function syncAllProducts(products) {
const results = await Promise.all(
products.map(product =>
limiter.schedule(() => updateShopifyProduct(product))
)
);
return results;
}
Bottleneck is excellent because it handles concurrency, rate limiting, and retries in one package.
Strategy 4: Batch Inventory Updates
For inventory syncs, use Shopify's inventory level set endpoint with batching:
async function batchInventoryUpdate(updates) {
const BATCH_SIZE = 10;
for (let i = 0; i < updates.length; i += BATCH_SIZE) {
const batch = updates.slice(i, i + BATCH_SIZE);
await Promise.all(
batch.map(({ inventoryItemId, locationId, quantity }) =>
limiter.schedule(() =>
shopifyRequest('inventory_levels/set.json', {
method: 'POST',
body: JSON.stringify({
location_id: locationId,
inventory_item_id: inventoryItemId,
available: quantity
})
})
)
)
);
console.log(`Updated ${Math.min(i + BATCH_SIZE, updates.length)}/${updates.length}`);
}
}
Strategy 5: Webhook-Driven Architecture
Instead of polling Shopify for changes, use webhooks to react to events:
// Register webhooks on app install
const WEBHOOK_TOPICS = [
'orders/create',
'products/update',
'inventory_levels/update'
];
for (const topic of WEBHOOK_TOPICS) {
await shopifyRequest('webhooks.json', {
method: 'POST',
body: JSON.stringify({
webhook: {
topic,
address: `${APP_URL}/webhooks/${topic.replace('/', '-')}`,
format: 'json'
}
})
});
}
Webhooks eliminate the need for constant polling, saving thousands of API calls per day.
Monitoring Your API Usage
Track your API call patterns to identify optimization opportunities:
let apiCallLog = [];
function logApiCall(endpoint, cost) {
apiCallLog.push({
endpoint,
cost,
timestamp: Date.now()
});
// Alert if approaching limits
const recentCalls = apiCallLog.filter(
c => c.timestamp > Date.now() - 60000
);
if (recentCalls.length > 100) {
console.warn(`High API usage: ${recentCalls.length} calls in last minute`);
}
}
Summary
| Strategy | Best For | API Savings | |----------|---------|-------------| | Respect headers | All API calls | Prevents 429 errors | | GraphQL bulk ops | Large data exports | 90%+ reduction | | Queue + throttle | Real-time syncs | Smooth throughput | | Batch updates | Inventory syncs | 50-80% reduction | | Webhooks | Event-driven updates | 95%+ reduction |
Combine these strategies based on your use case. Most production Shopify integrations need all five.