Rate Limits

ComplianceLayer enforces rate limits to ensure fair usage and protect API stability. Limits are based on your subscription plan and apply across multiple time windows.

Rate Limits vs Scan Quotas

ComplianceLayer has two separate usage controls, and they fail in different ways:

  • Monthly Scan Quota: How many security scans you can run per month — 10 on Free, 50 on Solo, 1,000 on Pro, 1,500 on MSP, 5,000 on Enterprise. This is your primary usage limit.
  • Rate Limits: The maximum API requests per time window. These prevent abuse and excessive polling, and are set high enough not to interfere with normal usage.

In practice: Your monthly scan quota is the limiting factor for most callers. Rate limits are only hit when making unusually high numbers of API calls, such as very frequent polling or an automated script without pacing.

Rate limiting applies to write requests. GET requests — polling job status, reading reports, listing domains — pass through the rate limiter and do not carry x-ratelimit-* headers. The counters and headers described below apply to POST, PUT, PATCH, and DELETE.

Rate Limit Tiers

PlanPer MinutePer HourPer DayConcurrentBurstScans / Month
Free1201,2005,00052010
Solo1502,00010,00082550
Pro30010,00050,00020501,000
MSP45020,000150,00040801,500
Enterprise60030,000200,000501005,000
Custom1,00050,000500,000100200Unlimited

Solo and MSP are legacy tiers retained for existing customers. All windows are enforced at once, so the tightest one that you are currently over is the one that rejects the request.

Rate Limit Headers

Rate-limited responses carry three headers describing the window that was checked:

HTTP/1.1 202 Accepted
x-ratelimit-limit: 120
x-ratelimit-remaining: 118
x-ratelimit-reset: 1785088320

# limit: Maximum requests allowed in the current window
# remaining: Requests left before hitting the limit
# reset: Unix timestamp when the window resets

Those three are the only rate limit headers. There is no header reporting your plan — read GET /v1/usage/limits if you need the plan and its limits. HTTP/2 lowercases all header names; header lookups are case-insensitive in every mainstream client, so X-RateLimit-Limit and x-ratelimit-limit both work.

Handling 429 Responses

A 429 Too Many Requests has two distinct causes, and they call for opposite reactions. Distinguish them by the presence of Retry-After.

Rate limit exceeded — retry

HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 120
x-ratelimit-remaining: 0
x-ratelimit-reset: 1785088380
Retry-After: 45

{
  "detail": "Rate limit exceeded for free plan. Limit: 120 per minute",
  "error": "Rate limit exceeded",
  "message": "Rate limit exceeded for free plan. Limit: 120 per minute",
  "retry_after": 45,
  "upgrade_url": "/pricing"
}

retry_after is in seconds and matches the Retry-After header. upgrade_url is null on Enterprise. Waiting out the window and retrying will succeed.

Scan quota exhausted — do not retry

When you have used your monthly scans, scan submissions also return 429, but with a plain detail string and no Retry-After header or retry_after field:

HTTP/1.1 429 Too Many Requests

{
  "detail": "Quota exceeded. Used 10/10 scans this period. Upgrade your plan or wait until 2026-08-01."
}
Quota exhaustion is terminal. No amount of backoff will make it succeed before your period resets. A retry helper that blindly sleeps and retries every 429 will spin until it exhausts its attempts. Branch on Retry-After: present means retry, absent means stop and surface the error.

Implementing Backoff Correctly

class QuotaExhaustedError extends Error {}

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

    if (response.ok) {
      return await response.json();
    }

    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After');

      // No Retry-After means the monthly scan quota is gone, not a rate limit.
      // Retrying cannot succeed until the billing period resets.
      if (!retryAfter) {
        const { detail } = await response.json();
        throw new QuotaExhaustedError(detail);
      }

      console.log(`Rate limited. Waiting ${retryAfter}s...`);
      await new Promise(r => setTimeout(r, parseInt(retryAfter) * 1000));
      continue;
    }

    throw new Error(`Request failed: ${response.status}`);
  }

  throw new Error('Max retries exceeded');
}

// Usage
const result = await fetchWithRetry(
  'https://api.compliancelayer.net/v1/scan',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ domain: 'example.com' })
  }
);

Monitoring Your Usage

Track rate limit headroom in real time from the response headers of your write requests:

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

  // Present on rate-limited (non-GET) responses
  const limit = parseInt(response.headers.get('x-ratelimit-limit'));
  const remaining = parseInt(response.headers.get('x-ratelimit-remaining'));
  const reset = parseInt(response.headers.get('x-ratelimit-reset'));

  if (!Number.isNaN(limit)) {
    const used = limit - remaining;
    const usagePercent = (used / limit * 100).toFixed(1);

    console.log(`Rate limit: ${used}/${limit} (${usagePercent}% used)`);
    console.log(`Resets at: ${new Date(reset * 1000).toISOString()}`);

    // Warn if approaching limit
    if (remaining < limit * 0.1) {
      console.warn('⚠️  Approaching rate limit! Consider slowing down requests.');
    }
  }

  return await response.json();
}

For scan quota rather than request rate, read GET /v1/usage/limits — see Usage Analytics.

Best Practices

1. Spread Out Requests

Don't burst all your requests at once. Spread them evenly across the time window:

class RateLimiter {
  constructor(requestsPerMinute) {
    this.requestsPerMinute = requestsPerMinute;
    this.queue = [];
    this.processing = false;
  }

  async request(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;
    const delay = 60000 / this.requestsPerMinute; // ms between requests

    while (this.queue.length > 0) {
      const { fn, resolve, reject } = this.queue.shift();

      try {
        const result = await fn();
        resolve(result);
      } catch (error) {
        reject(error);
      }

      // Wait before next request
      if (this.queue.length > 0) {
        await new Promise(r => setTimeout(r, delay));
      }
    }

    this.processing = false;
  }
}

// Usage
const limiter = new RateLimiter(120); // 120 req/min on the Free plan

for (const domain of domains) {
  await limiter.request(() =>
    fetch('https://api.compliancelayer.net/v1/scan', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ domain })
    })
  );
}

2. Cache Results

Cache scan results to avoid unnecessary API calls. A good strategy:

  • Cache results for 1-24 hours depending on domain criticality
  • Re-scan on a schedule rather than on every page load
  • Invalidate cache on-demand when needed

3. Use Webhooks Instead of Polling

Use webhooks to receive scan results instead of polling for them. This dramatically reduces API calls.

4. Batch Operations

Use batch endpoints to assess many domains in one request instead of one request per domain. Note that batches still consume one scan of quota per domain.

5. Monitor and Alert

Set up monitoring to alert you when:

  • You're consistently hitting rate limits
  • Usage approaches your plan's scan quota
  • Unusual spikes in API calls occur

Upgrading Your Plan

If you consistently hit rate limits or run out of scans, consider upgrading:

  • Free → Solo: 1.25x request rate (120 → 150/min) and 50 scans/month
  • Solo → Pro: 2x request rate (150 → 300/min) and 1,000 scans/month
  • Pro → Enterprise: 2x request rate (300 → 600/min) and 5,000 scans/month
  • Enterprise → Custom: Custom limits and pricing

Visit your billing settings to upgrade.

Enterprise Plans

Need higher limits? Enterprise plans offer:

  • Custom rate limits tailored to your needs
  • Priority support

Contact sales@compliancelayer.net to discuss enterprise pricing.