Fixing Shopify API Rate Limits (429), Timeouts, and Errors
Comprehensive guide to resolving Shopify API 429 Too Many Requests, handling 401/403 auth issues, fixing timeouts, and managing webhook failures.
- Shopify REST API uses a leaky bucket algorithm (40 requests max, 2/sec refill).
- GraphQL API uses a calculated query cost system (1000 points max, 50/sec refill).
- Webhooks fail if not acknowledged with a 200 OK within 5 seconds. Offload processing to a background worker.
- Implement exponential backoff and retry logic for 429 and 5xx errors.
| Method | When to Use | Time | Risk |
|---|---|---|---|
| Leaky Bucket Queue | REST API integration | Medium | Low |
| GraphQL Cost Optimization | GraphQL API queries | High | Low |
| Exponential Backoff | Handling 5xx and 429s | Low | Low |
| Background Workers | Webhook processing | Medium | Low |
Understanding the Error
When working with Shopify's APIs, you will inevitably encounter rate limits or transient errors. Shopify aggressively throttles API requests to ensure platform stability. The most common manifestation of this is the 429 Too Many Requests HTTP status code.
Common Error Messages
HTTP 429 Too Many Requests{"errors":"Exceeded 2 calls per second for api client. Reduce request rates to resume uninterrupted service."}- GraphQL:
{"errors":[{"message":"Throttled"}]}
Other related errors include:
- 401 Unauthorized: Invalid or expired
X-Shopify-Access-Token. - 403 Forbidden: Your app lacks the required access scopes (e.g., trying to read orders without
read_ordersscope). - 500, 502, 503: Server-side errors from Shopify. These require retry logic.
- Webhooks not working: Often caused by your endpoint taking longer than 5 seconds to respond, causing Shopify to timeout and eventually drop the webhook subscription.
Step 1: Diagnose Rate Limits
Before fixing, you need to know which API you are hitting and what your limits are.
REST API:
Shopify includes a special header in every REST response: X-Shopify-Shop-Api-Call-Limit. It looks like X-Shopify-Shop-Api-Call-Limit: 39/40. This means you have used 39 out of 40 available calls in your bucket. If you hit 40/40, the next request will trigger a 429 error.
GraphQL API:
GraphQL limits are based on query complexity. Check the extensions.cost object in your GraphQL response. It shows requestedQueryCost, actualQueryCost, and your throttleStatus (currentlyAvailable, maximumAvailable, restoreRate).
Step 2: Implement the Fix
1. Handling REST Limits (Leaky Bucket)
Standard Shopify plans allow a bucket size of 40 requests, which empties (refills your allowance) at a rate of 2 requests per second. Shopify Plus stores get 80 requests and a refill rate of 4 per second.
You must build a queueing system or a client-side rate limiter that tracks the X-Shopify-Shop-Api-Call-Limit header and pauses execution when you approach the limit (e.g., pausing when you hit 35/40).
2. Handling GraphQL Limits
You start with 1000 points and regain 50 points per second. To optimize:
- Only request the exact fields you need.
- Reduce the
firstorlastarguments in pagination. - If a query costs more than your available capacity, pause your script based on the required restore rate before making the call.
3. Fixing Webhook Timeouts
Shopify expects a 200 OK response within 5 seconds of dispatching a webhook. If you process the payload synchronously (e.g., creating a user in your database, resizing an image), you will likely timeout.
Fix: Immediately return a 200 OK upon receiving the webhook, and push the actual processing logic into a background queue like Celery, Sidekiq, or AWS SQS.
4. Handling 5xx and Transient Errors
Implement Exponential Backoff with Jitter. When you receive a 429, 500, 502, or 503 error, wait for 1 second, then retry. If it fails again, wait 2 seconds, then 4 seconds, etc., up to a reasonable maximum.
Frequently Asked Questions
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_shopify_session():
session = requests.Session()
# Define retry strategy for 429 and 5xx errors
retry_strategy = Retry(
total=5, # Maximum number of retries
backoff_factor=1, # Exponential backoff factor (1s, 2s, 4s...)
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def make_safe_request(url, headers):
session = get_shopify_session()
response = session.get(url, headers=headers)
# Manual tracking for REST leaky bucket
if 'X-Shopify-Shop-Api-Call-Limit' in response.headers:
usage, limit = map(int, response.headers['X-Shopify-Shop-Api-Call-Limit'].split('/'))
print(f"API Usage: {usage}/{limit}")
# If we are close to the limit, proactively sleep
if usage >= (limit - 5):
print("Approaching rate limit, sleeping for 2 seconds...")
time.sleep(2)
response.raise_for_status()
return response.json()Error Medic Editorial
Error Medic Editorial is a team of senior SREs and DevOps engineers dedicated to providing actionable, code-first solutions to complex infrastructure and API problems.