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

HeaderDescriptionExample
RateLimit-RemainingNumber of requests remaining in the current 1-second window3 (you can make 3 more requests)
RateLimit-ResetUnix timestamp when the limit resets1609459201 (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:

  1. Pause immediately — Don't make more requests
  2. Wait before retrying — Use a backoff strategy
  3. 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 response

Recommended 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 request

Exponential 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 request

Best 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:00Z

Use appropriate page sizes:

# Fetch 100 products per request (max allowed)
GET /core/v3/stores/{store_id}/products?limit=100

2. 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/second

Batch 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 batches

4. 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 response

5. 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 created

6. 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 products

Common scenarios

Bulk data sync

Problem: Syncing 10,000 products would take 2,000 seconds (33 minutes) at 5 req/sec.

Solution:

  1. Use pagination efficiently — Fetch 100 products per request (max limit)
  2. Run during off-peak hours — Sync overnight or during low-traffic periods
  3. Sync incrementally — Use updated_at_min to fetch only changed products
  4. 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:

  1. Queue requests — Use a job queue to smooth bursts
  2. Monitor RateLimit-Remaining — Throttle proactively
  3. 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:

  1. Coordinate across systems — Share rate limit state
  2. Centralize API calls — Route all requests through one service
  3. Prioritize critical operations — Ensure high-priority calls succeed

Troubleshooting

Hitting 429s frequently

Check:

  1. Are you making unnecessary requests? (Use caching)
  2. Are multiple systems using the same Store ID?
  3. Are you bursting requests? (Distribute evenly)
  4. 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:

  1. Add time.sleep(0.2) between requests (ensures ≤5 req/sec)
  2. Monitor RateLimit-Remaining and pause when needed
  3. 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-Remaining and RateLimit-Reset headers
  • Wait 1 second after receiving 429, then retry
  • Optimize requests: cache, batch, filter, use webhooks
  • Distribute requests evenly — avoid bursts

Related


Did this page help you?