Overview

The ProBeya API enforces rate limits to ensure fair usage and platform stability. Limits are applied per API key using a sliding window algorithm backed by Redis. Every response includes rate limit headers so your application always knows its current standing.

Limits by Plan

PlanRequests per MinuteBurst AllowanceConcurrent Connections
Free100205
Pro1,00010025
Enterprise10,000500100

Burst allowance allows short spikes above the per-minute rate. For example, a Pro plan key can send up to 100 requests in a 1-second burst before the sliding window kicks in.

Response Headers

Every API response includes three rate limit headers:

HeaderTypeDescription
X-RateLimit-LimitnumberMaximum requests allowed in the current window
X-RateLimit-RemainingnumberRequests remaining before throttling begins
X-RateLimit-ResetnumberUnix timestamp (seconds) when the window resets

Example response headers:

HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 947
X-RateLimit-Reset: 1711032060

When You Hit the Limit

When you exceed the rate limit, the API returns a 429 Too Many Requests response with a Retry-After header indicating how many seconds to wait:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 12
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711032060
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "status": 429,
    "message": "Rate limit exceeded. Retry after 12 seconds.",
    "retryAfter": 12
  }
}

Retry Strategy

Implement exponential backoff with jitter to avoid thundering-herd effects when multiple clients are throttled simultaneously:

async function fetchWithRetry(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    // If not rate-limited, return immediately
    if (response.status !== 429) {
      return response;
    }

    // Read the Retry-After header (in seconds) or fall back to exponential backoff
    const retryAfter = parseInt(response.headers.get("Retry-After") || "0", 10);
    const backoff = retryAfter > 0
      ? retryAfter * 1000
      : Math.min(1000 * Math.pow(2, attempt), 30000);

    // Add random jitter (0-25% of backoff) to prevent synchronized retries
    const jitter = Math.random() * backoff * 0.25;
    const delay = backoff + jitter;

    console.warn(
      `Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +
      `Retrying in ${Math.round(delay)}ms...`
    );

    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error("Max retries exceeded due to rate limiting");
}

Proactive Rate Limit Monitoring

Rather than waiting for a 429, monitor the X-RateLimit-Remaining header and slow down before hitting the limit:

async function fetchWithThrottle(url, options) {
  const response = await fetch(url, options);

  const remaining = parseInt(
    response.headers.get("X-RateLimit-Remaining") || "999",
    10
  );
  const resetAt = parseInt(
    response.headers.get("X-RateLimit-Reset") || "0",
    10
  );

  // If fewer than 10% of requests remain, throttle proactively
  const limit = parseInt(
    response.headers.get("X-RateLimit-Limit") || "1000",
    10
  );
  if (remaining < limit * 0.1) {
    const waitMs = Math.max(0, (resetAt * 1000) - Date.now());
    console.warn(
      `Only ${remaining} requests remaining. ` +
      `Pausing ${Math.round(waitMs / 1000)}s until window resets.`
    );
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }

  return response;
}

Best Practices