Rate limiting
How to handle rate limits and avoid 429 errors when making API requests.
The Core API enforces rate limits to ensure reliable performance for all users. This guide explains how rate limits work and how to handle them in your integration.
How rate limiting works
Limit: 5 requests per second per store
The limit applies to your Store ID (app_key). All API requests using the same Store ID count toward this limit, regardless of endpoint or access token.
What happens when you exceed the limit
If you make more than 5 requests in a 1-second window, you'll receive:
HTTP 429 Too Many Requests
{
"errors": [{
"message": "Too Many Requests"
}]
}The request is not processed. You must retry after waiting.
Monitor your rate limit usage
Every API response includes headers showing your current rate limit status:
RateLimit-Remaining: 3
RateLimit-Reset: 1609459201
Headers explained
| Header | Description | Example |
|---|---|---|
RateLimit-Remaining | Number of requests remaining in the current 1-second window | 3 (you can make 3 more requests) |
RateLimit-Reset | Unix timestamp when the limit resets | 1609459201 (resets at this second) |
Reading the headers
curl -i --request GET \
--url https://api.yotpo.com/core/v3/stores/{store_id}/products \
--header 'X-Yotpo-Token: YOUR_ACCESS_TOKEN'Response headers:
HTTP/1.1 200 OK
RateLimit-Remaining: 4
RateLimit-Reset: 1609459201
...
Check RateLimit-Remaining after each request. When it reaches 0, wait until RateLimit-Reset before making the next request.
Handle 429 errors
When you receive a 429 error, your integration should:
- Pause immediately — Don't make more requests
- Wait before retrying — Use a backoff strategy
- Retry the request — Once the wait period ends
Basic retry logic
import time
import requests
def make_request(url, headers):
response = requests.get(url, headers=headers)
if response.status_code == 429:
# Wait 1 second and retry
time.sleep(1)
return make_request(url, headers)
return responseRecommended backoff strategy
Fixed 1-second wait (simplest approach):
Wait 1 second after every 429, then retry. Since the rate limit resets every second, this ensures you're within a fresh window.
if response.status_code == 429:
time.sleep(1)
# Retry requestExponential backoff (for burst scenarios):
If you're hitting 429s repeatedly, exponential backoff prevents overwhelming the API:
import time
def make_request_with_backoff(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code != 429:
return response
# Exponential backoff: 1s, 2s, 4s
wait_time = 2 ** attempt
time.sleep(wait_time)
raise Exception("Max retries exceeded")Use RateLimit-Reset header (most precise):
Read the reset timestamp and wait until that exact moment:
if response.status_code == 429:
reset_time = int(response.headers.get('RateLimit-Reset', 0))
current_time = int(time.time())
wait_time = max(reset_time - current_time, 1)
time.sleep(wait_time)
# Retry requestBest practices
1. Optimize your requests
Fetch only what you need:
# ❌ Bad: Fetch all products every time
GET /core/v3/stores/{store_id}/products
# ✅ Good: Filter by last update time
GET /core/v3/stores/{store_id}/products?updated_at_min=2024-01-01T00:00:00ZUse appropriate page sizes:
# Fetch 100 products per request (max allowed)
GET /core/v3/stores/{store_id}/products?limit=1002. Cache responses
Store frequently accessed data locally to reduce API calls:
import time
cache = {}
CACHE_TTL = 300 # 5 minutes
def get_products(store_id, token):
cache_key = f"products_{store_id}"
# Check cache
if cache_key in cache:
data, timestamp = cache[cache_key]
if time.time() - timestamp < CACHE_TTL:
return data
# Fetch from API
response = make_request(url, headers)
cache[cache_key] = (response.json(), time.time())
return response.json()3. Distribute requests evenly
Avoid bursts:
# ❌ Bad: Process 100 orders at once
for order in orders:
create_order(order)
# ✅ Good: Add delay between requests
for order in orders:
create_order(order)
time.sleep(0.2) # 5 requests/secondBatch operations across time:
Instead of syncing your entire catalog at once, spread updates over time:
# Sync 500 products per minute (well under 5 req/sec)
for batch in product_batches:
for product in batch:
update_product(product)
time.sleep(0.2)
time.sleep(60) # Wait 1 minute between batches4. Monitor RateLimit-Remaining
Proactively throttle before hitting the limit:
def make_smart_request(url, headers):
response = requests.get(url, headers=headers)
remaining = int(response.headers.get('RateLimit-Remaining', 5))
# If only 1 request left, wait before next call
if remaining <= 1:
time.sleep(0.5)
return response5. Use webhooks for real-time updates
Instead of polling for changes, subscribe to webhooks to receive notifications:
# ❌ Bad: Poll for new reviews every minute
while True:
reviews = get_reviews()
time.sleep(60)
# ✅ Good: Subscribe to review_create webhook
# Yotpo pushes notifications when reviews are created6. Implement graceful degradation
If rate-limited, don't fail completely:
def sync_products():
try:
products = fetch_products()
update_local_catalog(products)
except RateLimitError:
# Log and continue with stale data
log.warning("Rate limited, using cached data")
products = load_from_cache()
return productsCommon scenarios
Bulk data sync
Problem: Syncing 10,000 products would take 2,000 seconds (33 minutes) at 5 req/sec.
Solution:
- Use pagination efficiently — Fetch 100 products per request (max
limit) - Run during off-peak hours — Sync overnight or during low-traffic periods
- Sync incrementally — Use
updated_at_minto fetch only changed products - Consider the aggregated endpoint — For orders, use Send aggregated order info to send all data in one call
Real-time order processing
Problem: Processing orders as they come in, unpredictable volume.
Solution:
- Queue requests — Use a job queue to smooth bursts
- Monitor RateLimit-Remaining — Throttle proactively
- Implement retry logic — Handle 429s gracefully with exponential backoff
from queue import Queue
import time
order_queue = Queue()
def process_orders():
while True:
if order_queue.empty():
time.sleep(0.1)
continue
order = order_queue.get()
try:
create_order(order)
time.sleep(0.2) # Rate limit safety margin
except RateLimitError:
# Put back in queue and wait
order_queue.put(order)
time.sleep(1)Multiple integrations
Problem: You have multiple systems hitting the API with the same Store ID.
Solution:
- Coordinate across systems — Share rate limit state
- Centralize API calls — Route all requests through one service
- Prioritize critical operations — Ensure high-priority calls succeed
Troubleshooting
Hitting 429s frequently
Check:
- Are you making unnecessary requests? (Use caching)
- Are multiple systems using the same Store ID?
- Are you bursting requests? (Distribute evenly)
- Are you retrying failed requests properly? (Check backoff logic)
Solution: Review the best practices above and optimize your integration.
RateLimit-Remaining always shows 0
Cause: You're consistently exceeding 5 requests per second.
Solution:
- Add
time.sleep(0.2)between requests (ensures ≤5 req/sec) - Monitor
RateLimit-Remainingand pause when needed - Reduce concurrent API calls if running multi-threaded
Different endpoints, same limit
Expected behavior: The 5 req/sec limit applies across all endpoints for your Store ID.
Creating a product and fetching an order both count toward the same limit.
Summary
Key takeaways:
- Limit is 5 requests/second per Store ID
- Monitor
RateLimit-RemainingandRateLimit-Resetheaders - Wait 1 second after receiving 429, then retry
- Optimize requests: cache, batch, filter, use webhooks
- Distribute requests evenly — avoid bursts
Related
- Guidelines and conventions — General API guidelines
- Error handling — How to handle other error types
- Webhooks — Real-time notifications (avoid polling)
Updated about 3 hours ago