API Rate Limiting & Optimization

StateSet implements rate limiting to ensure fair usage and maintain service reliability for all users. This guide covers understanding rate limits, implementing efficient request patterns, and optimizing your API usage.

Rate Limit Overview

StateSet uses a sliding window rate limiting algorithm that provides smooth, predictable limits across different time windows.

Per-Second Limits

Prevents burst traffic spikes and ensures responsive service

Per-Minute Limits

Controls sustained request volume over longer periods

Per-Hour Limits

Manages overall API consumption and prevents abuse

Current Rate Limits

By Plan Type

Endpoint CategoryPer SecondPer MinutePer Hour
Orders API103005,000
Customers API51502,500
Returns API51502,500
Webhooks2060010,000
Analytics2601,000

Special Considerations

Rate Limit Headers

Every API response includes rate limit information in the headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 1500
X-RateLimit-Remaining: 1247
X-RateLimit-Reset: 1640995200
X-RateLimit-Window: 60
X-RateLimit-Category: orders-api
Retry-After: 45

Header Descriptions

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
X-RateLimit-WindowWindow duration in seconds (60 for per-minute limits)
X-RateLimit-CategoryAPI category for this endpoint
Retry-AfterSeconds to wait before retrying (present when rate limited)

Implementing Rate Limit Handling

Basic Rate Limit Detection

import { StateSetClient } from 'stateset-node';
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()]
});

class RateLimitHandler {
  constructor(client) {
    this.client = client;
    this.requestCounts = new Map();
  }

  async makeRequest(operation, ...args) {
    try {
      const response = await operation(...args);
      this.logRateLimitInfo(response);
      return response;
    } catch (error) {
      if (error.status === 429) {
        return this.handleRateLimit(error, operation, ...args);
      }
      throw error;
    }
  }

  logRateLimitInfo(response) {
    const headers = response.headers || {};
    logger.info('Rate limit status', {
      remaining: headers['x-ratelimit-remaining'],
      limit: headers['x-ratelimit-limit'],
      resetTime: new Date(headers['x-ratelimit-reset'] * 1000),
      category: headers['x-ratelimit-category']
    });
  }

  async handleRateLimit(error, operation, ...args) {
    const retryAfter = error.headers?.['retry-after'] || 60;
    
    logger.warn('Rate limit exceeded, waiting before retry', {
      retryAfter,
      category: error.headers?.['x-ratelimit-category']
    });

    await this.sleep(retryAfter * 1000);
    return this.makeRequest(operation, ...args);
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const client = new StateSetClient({ apiKey: process.env.STATESET_API_KEY });
const rateLimitHandler = new RateLimitHandler(client);

const orders = await rateLimitHandler.makeRequest(
  () => client.orders.list({ limit: 50 })
);

Advanced Rate Limiting with Queue

import PQueue from 'p-queue';

class AdvancedRateLimitHandler {
  constructor(client, options = {}) {
    this.client = client;
    this.queues = new Map();
    this.rateLimitInfo = new Map();
    
    // Default queue options
    this.defaultQueueOptions = {
      concurrency: 5,
      interval: 1000,
      intervalCap: 10,
      ...options.queueOptions
    };
  }

  getQueue(category = 'default') {
    if (!this.queues.has(category)) {
      const queueOptions = this.getCategoryQueueOptions(category);
      this.queues.set(category, new PQueue(queueOptions));
    }
    return this.queues.get(category);
  }

  getCategoryQueueOptions(category) {
    const categorySettings = {
      'orders-api': { concurrency: 10, interval: 1000, intervalCap: 10 },
      'customers-api': { concurrency: 5, interval: 1000, intervalCap: 5 },
      'returns-api': { concurrency: 5, interval: 1000, intervalCap: 5 },
      'analytics': { concurrency: 2, interval: 1000, intervalCap: 2 }
    };

    return {
      ...this.defaultQueueOptions,
      ...categorySettings[category]
    };
  }

  async makeRequest(operation, category = 'default', priority = 0) {
    const queue = this.getQueue(category);
    
    return queue.add(async () => {
      try {
        const response = await operation();
        this.updateRateLimitInfo(category, response);
        return response;
      } catch (error) {
        if (error.status === 429) {
          await this.handleRateLimit(error, category);
          throw error; // Re-queue will happen automatically
        }
        throw error;
      }
    }, { priority });
  }

  updateRateLimitInfo(category, response) {
    const headers = response.headers || {};
    this.rateLimitInfo.set(category, {
      remaining: parseInt(headers['x-ratelimit-remaining']) || 0,
      limit: parseInt(headers['x-ratelimit-limit']) || 1000,
      resetTime: parseInt(headers['x-ratelimit-reset']) || 0,
      lastUpdated: Date.now()
    });
  }

  async handleRateLimit(error, category) {
    const retryAfter = parseInt(error.headers?.['retry-after']) || 60;
    const queue = this.getQueue(category);
    
    logger.warn('Rate limit exceeded for category', {
      category,
      retryAfter,
      queueSize: queue.size,
      pending: queue.pending
    });

    // Pause the queue
    queue.pause();
    
    await this.sleep(retryAfter * 1000);
    
    // Resume the queue
    queue.start();
  }

  getRateLimitStatus(category = 'default') {
    return this.rateLimitInfo.get(category) || null;
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage with priority queueing
const handler = new AdvancedRateLimitHandler(client);

// High priority request (emergency order processing)
const criticalOrder = await handler.makeRequest(
  () => client.orders.get(orderId),
  'orders-api',
  10
);

// Normal priority batch processing
const orders = await Promise.all([
  handler.makeRequest(() => client.orders.list({ page: 1 }), 'orders-api', 0),
  handler.makeRequest(() => client.orders.list({ page: 2 }), 'orders-api', 0),
  handler.makeRequest(() => client.orders.list({ page: 3 }), 'orders-api', 0)
]);

Optimization Strategies

1. Efficient Pagination

Instead of requesting large datasets at once:

// ❌ Inefficient - May trigger rate limits
const allOrders = await client.orders.list({ limit: 1000 });

// ✅ Efficient - Paginated requests
async function getAllOrdersPaginated() {
  const allOrders = [];
  let hasMore = true;
  let cursor = null;

  while (hasMore) {
    const response = await client.orders.list({
      limit: 100,
      cursor: cursor
    });

    allOrders.push(...response.data);
    cursor = response.next_cursor;
    hasMore = response.has_more;

    // Small delay to respect rate limits
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  return allOrders;
}

2. Batch Operations

Use batch endpoints when available:

// ❌ Multiple individual requests
for (const order of orders) {
  await client.orders.update(order.id, { status: 'processed' });
}

// ✅ Single batch request
await client.orders.batchUpdate(
  orders.map(order => ({
    id: order.id,
    status: 'processed'
  }))
);

3. Webhook-First Architecture

Reduce polling by using webhooks:

// ❌ Constant polling
setInterval(async () => {
  const orders = await client.orders.list({ 
    status: 'pending',
    updated_since: lastCheck 
  });
  processNewOrders(orders);
}, 30000);

// ✅ Webhook-driven updates
app.post('/webhooks/stateset', (req, res) => {
  const event = req.body;
  
  if (event.type === 'order.updated') {
    processOrderUpdate(event.data);
  }
  
  res.status(200).send('OK');
});

4. Intelligent Caching

Cache frequently requested data:

class StateSetCache {
  constructor(client, ttl = 300000) { // 5 minutes default TTL
    this.client = client;
    this.cache = new Map();
    this.ttl = ttl;
  }

  async get(key, fetchFunction) {
    const cached = this.cache.get(key);
    const now = Date.now();

    if (cached && (now - cached.timestamp) < this.ttl) {
      logger.debug('Cache hit', { key });
      return cached.data;
    }

    logger.debug('Cache miss, fetching', { key });
    const data = await fetchFunction();
    
    this.cache.set(key, {
      data,
      timestamp: now
    });

    return data;
  }

  clear(key) {
    if (key) {
      this.cache.delete(key);
    } else {
      this.cache.clear();
    }
  }
}

// Usage
const cache = new StateSetCache(client);

const customer = await cache.get(
  `customer:${customerId}`,
  () => client.customers.get(customerId)
);

Monitoring Rate Limits

Rate Limit Dashboard

Create a monitoring dashboard for your rate limit usage:

class RateLimitMonitor {
  constructor() {
    this.metrics = {
      requests: new Map(),
      rateLimits: new Map(),
      errors: new Map()
    };
  }

  recordRequest(category, success = true) {
    const key = `${category}:${this.getTimeWindow()}`;
    const current = this.metrics.requests.get(key) || { success: 0, total: 0 };
    
    current.total++;
    if (success) current.success++;
    
    this.metrics.requests.set(key, current);
  }

  recordRateLimit(category, remainingRequests, totalLimit) {
    const key = `${category}:${this.getTimeWindow()}`;
    this.metrics.rateLimits.set(key, {
      remaining: remainingRequests,
      total: totalLimit,
      utilizationPct: ((totalLimit - remainingRequests) / totalLimit) * 100,
      timestamp: Date.now()
    });
  }

  getTimeWindow() {
    return Math.floor(Date.now() / 60000); // 1-minute windows
  }

  getUtilizationReport() {
    const report = {};
    
    for (const [key, data] of this.metrics.rateLimits) {
      const [category] = key.split(':');
      if (!report[category]) report[category] = [];
      report[category].push(data);
    }

    return report;
  }

  // Alert when utilization is high
  checkAlerts() {
    const report = this.getUtilizationReport();
    
    for (const [category, dataPoints] of Object.entries(report)) {
      const latest = dataPoints[dataPoints.length - 1];
      
      if (latest.utilizationPct > 80) {
        logger.warn('High API utilization detected', {
          category,
          utilization: latest.utilizationPct,
          remaining: latest.remaining
        });
      }
    }
  }
}

// Usage with middleware
const monitor = new RateLimitMonitor();

const monitoredClient = new Proxy(client, {
  get(target, prop) {
    const original = target[prop];
    
    if (typeof original === 'object' && original !== null) {
      return new Proxy(original, {
        get(apiTarget, apiProp) {
          const apiMethod = apiTarget[apiProp];
          
          if (typeof apiMethod === 'function') {
            return async (...args) => {
              const category = `${prop}-api`;
              
              try {
                const result = await apiMethod.apply(apiTarget, args);
                monitor.recordRequest(category, true);
                
                if (result.headers) {
                  monitor.recordRateLimit(
                    category,
                    parseInt(result.headers['x-ratelimit-remaining']),
                    parseInt(result.headers['x-ratelimit-limit'])
                  );
                }
                
                return result;
              } catch (error) {
                monitor.recordRequest(category, false);
                throw error;
              }
            };
          }
          
          return apiMethod;
        }
      });
    }
    
    return original;
  }
});

Best Practices Summary

Request Optimization

  • Use pagination instead of large limit values
  • Implement batch operations where possible
  • Cache frequently requested data
  • Use webhooks to reduce polling

Error Handling

  • Always check rate limit headers
  • Implement exponential backoff for retries
  • Use queues to manage request flow
  • Monitor and alert on high utilization

Architecture Patterns

  • Design webhook-first integrations
  • Implement circuit breakers for resilience
  • Use separate queues for different priorities
  • Cache aggressively with smart invalidation

Monitoring & Alerting

  • Track utilization across all categories
  • Set alerts at 80% utilization threshold
  • Monitor request success rates
  • Review patterns during peak usage

Getting Help

If you’re consistently hitting rate limits or need higher limits:

  1. Review your usage patterns using the monitoring tools above
  2. Optimize your integration following the strategies in this guide
  3. Consider upgrading to a higher plan with increased limits
  4. Contact support at support@stateset.com for custom rate limits