Skip to content

Rate Limits

The License Server API implements intelligent rate limiting to protect the service while allowing legitimate usage patterns. Different endpoint categories have different rate limits based on their expected usage frequency.

Rate Limit Categories

High Frequency - Validation Endpoints

1000 requests / 15 minutes

These endpoints are designed for high-frequency real-time validation:

  • POST /v1/verify-license - Verify license validity
  • POST /v1/validate-license - Validate license for instance
  • POST /v1/track-usage - Report usage metrics
  • POST /v1/usage-report - Get usage history
  • POST /v1/check-update - License-gated software update check (Software Distribution)

Use case: Real-time license checks in your application, usage tracking, monitoring.

Moderate Frequency - Activation Endpoints

300 requests / 15 minutes

These endpoints handle license activation and file operations:

  • POST /v1/activate-license - Activate license for instance
  • POST /v1/deactivate-license - Free a seat so it can be activated elsewhere
  • POST /v1/verify-license-file - Verify license (JSON body, despite the name — not a file upload)
  • POST /v1/verify-license-file-base64 - Verify Base64 license file

Use case: License activation during application startup, moving a seat between machines, license file verification.

Additional per-key limit on activation

activate-license and deactivate-license carry a second, stricter limit on top of the 300/15min per-IP one above: 20 requests / 15 minutes, keyed per license key (not per IP). This exists because the license key alone authorizes activation — without it, one key could be hammered with activation guesses from many different IPs. If you're activating many instances of the same key in a short window (e.g. a bulk rollout), you can hit this limit well before the per-IP one.

Standard Frequency - Public Endpoints

500 requests / 15 minutes

Public information endpoints with generous limits:

  • GET /v1/export-license/{key} - Get license information
  • GET /v1/export-license/{key}/file - Download license file
  • GET /v1/export-license/{key}/offline - Get an RSA-signed license, verifiable offline with only a public key
  • GET /v1/public-key - Get the RSA public key used for offline verification

Use case: Exporting license data, downloading license files, offline license verification.

Low Frequency - Management Endpoints

100 requests / 15 minutes (Authentication Required)

Sensitive license management operations:

  • POST /v1/issue-license - Create new license
  • POST /v1/revoke-license - Revoke/reactivate license
  • POST /v1/admin/update-license-terms - Update a license's expires_at, max_activations, or limits
  • DELETE /v1/delete-license - Delete license
  • POST /v1/register-release - Register a software release (Software Distribution)
  • POST /v1/unpublish-release - Unpublish a software release (Software Distribution)

Use case: Issuing, revoking, and deleting licenses; registering and unpublishing software releases.

Admin Reporting Endpoints

300 requests / 15 minutes (Authentication Required) — the same numeric limit as the Activation tier above, not lower despite coming after "Low Frequency" on this page; the two just aren't on the same ordinal scale (this one is configurable, the others aren't).

Administrative reporting operations. Unlike every other tier on this page, this limit is configurable via the ADMIN_RATE_LIMIT_MAX environment variable — 300 is just the default:

  • GET /v1/list-licenses - List all licenses
  • GET /v1/list-releases - List your registered software releases (Software Distribution)
  • GET /v1/release/{id} - Fetch a single software release, including its signature (Software Distribution)
  • GET /v1/admin/stats - Get admin statistics
  • GET /v1/recent-activations - Get recent activations
  • GET /v1/list-activations/{key} - List activations for a specific license key
  • GET /v1/admin/license/{key} - Look up a single license's full record
  • POST /v1/admin/update-notes - Update a license's internal notes
  • POST /v1/admin/reissue-token - Reissue an activation token
  • POST /v1/admin/deactivate-by-instance-id - Force-free a seat by instance_id, without its token
  • POST /v1/admin/reset-usage - Reset a license's tracked usage
  • GET /v1/admin/build-info - View build/watermark info
  • GET /v1/billing/status - Check the calling tenant's subscription plan/status (hosted tier)
  • POST /v1/billing/checkout - Start an upgrade to a paid plan (hosted tier)
  • POST /v1/rotate-api-key - Rotate the calling tenant's own API key (hosted tier)

Use case: Administration dashboards, periodic reporting, auditing per-license device activity.

Rate Limit Headers

Every API response includes rate limit information in the headers:

http
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the time window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets

Handling Rate Limits

When You Hit the Limit

When you exceed the rate limit, you'll receive a 429 Too Many Requests response:

http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 60

{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Try again in 60 seconds.",
  "retryAfter": 60
}

Best Practices

1. Respect Rate Limits

Always check rate limit headers and implement backoff:

javascript
async function makeAPIRequest(url, options) {
  const response = await fetch(url, options);
  
  // Check rate limit headers
  const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
  const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
  
  if (response.status === 429) {
    const retryAfter = parseInt(response.headers.get('Retry-After')) * 1000;
    console.warn(`Rate limit exceeded. Retrying in ${retryAfter}ms`);
    await new Promise(resolve => setTimeout(resolve, retryAfter));
    return makeAPIRequest(url, options); // Retry
  }
  
  // Warn when approaching limit
  if (remaining < 10) {
    console.warn(`Approaching rate limit: ${remaining} requests remaining`);
  }
  
  return response;
}

2. Implement Exponential Backoff

javascript
class LicenseClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.maxRetries = 3;
  }

  async request(endpoint, options, retryCount = 0) {
    try {
      const response = await fetch(`${this.baseURL}${endpoint}`, options);
      
      if (response.status === 429 && retryCount < this.maxRetries) {
        const retryAfter = parseInt(response.headers.get('Retry-After')) || 1;
        const delay = Math.min(1000 * Math.pow(2, retryCount), retryAfter * 1000);
        
        console.log(`Rate limited. Retrying in ${delay}ms (attempt ${retryCount + 1})`);
        await new Promise(resolve => setTimeout(resolve, delay));
        
        return this.request(endpoint, options, retryCount + 1);
      }
      
      return response;
    } catch (error) {
      if (retryCount < this.maxRetries) {
        const delay = 1000 * Math.pow(2, retryCount);
        await new Promise(resolve => setTimeout(resolve, delay));
        return this.request(endpoint, options, retryCount + 1);
      }
      throw error;
    }
  }
}

3. Cache License Verification Results

javascript
class CachedLicenseClient {
  constructor(baseURL, cacheTimeout = 300000) { // 5 minutes
    this.baseURL = baseURL;
    this.cache = new Map();
    this.cacheTimeout = cacheTimeout;
  }

  async verifyLicense(key) {
    const cacheKey = `verify:${key}`;
    const cached = this.cache.get(cacheKey);
    
    // Return cached result if still valid
    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return cached.result;
    }
    
    // Make API request
    const response = await fetch(`${this.baseURL}/verify-license`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key })
    });
    
    const result = await response.json();
    
    // Cache successful results
    if (response.ok && result.valid) {
      this.cache.set(cacheKey, {
        result,
        timestamp: Date.now()
      });
    }
    
    return result;
  }
}

4. Batch Operations When Possible

Instead of making many individual requests, batch operations:

javascript
// ❌ Bad: Multiple individual requests
for (const key of licenseKeys) {
  await verifyLicense(key);
}

// ✅ Good: Batch verification (if your app supports it)
const results = await Promise.all(
  licenseKeys.map(key => verifyLicense(key))
);

// ✅ Better: Use rate-aware batching
async function batchVerifyLicenses(keys, batchSize = 10, delayBetweenBatches = 1000) {
  const results = [];
  
  for (let i = 0; i < keys.length; i += batchSize) {
    const batch = keys.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(key => verifyLicense(key))
    );
    results.push(...batchResults);
    
    // Add delay between batches to stay within rate limits
    if (i + batchSize < keys.length) {
      await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
    }
  }
  
  return results;
}

Rate Limiting by IP

Rate limits are applied per IP address on every tier, with one exception: activate-license and deactivate-license are additionally limited per license key (see the warning above). This means:

  • Each client IP gets its own rate limit bucket
  • Multiple users behind the same NAT/proxy share limits
  • Load balancers should preserve client IP addresses

Handling Shared IP Scenarios

If multiple users share an IP (corporate NAT, shared hosting), consider:

  1. User-based rate limiting (custom implementation)
  2. API key based limits (for authenticated users)
  3. Request higher limits for shared environments

Monitoring Rate Limits

Client-Side Monitoring

Track your rate limit usage:

javascript
class RateLimitMonitor {
  constructor() {
    this.limits = new Map();
  }

  trackResponse(endpoint, response) {
    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'));
    
    this.limits.set(endpoint, {
      limit,
      remaining,
      reset,
      usagePercent: ((limit - remaining) / limit * 100).toFixed(1)
    });
    
    // Log high usage
    if (remaining < limit * 0.1) { // Less than 10% remaining
      console.warn(`High rate limit usage for ${endpoint}: ${remaining}/${limit} remaining`);
    }
  }

  getLimitStatus(endpoint) {
    return this.limits.get(endpoint);
  }

  getAllLimitStatus() {
    return Object.fromEntries(this.limits);
  }
}

Server-Side Monitoring

If you run the License Server, monitor rate limit metrics:

  • Requests per endpoint category
  • Rate limit violations by IP
  • Peak usage times
  • Client retry patterns

Fixed Rate Limits

Every tier's limit is a fixed value set in the server implementation, except the Admin-Info tier, which is configurable via the ADMIN_RATE_LIMIT_MAX environment variable (default 300 requests / 15 minutes).

Need Higher Limits?

If your application requires higher rate limits:

  1. Optimize your usage - Cache results, batch requests
  2. Contact support - For enterprise license limits
  3. Self-host - Deploy your own instance with custom limits
  4. Use multiple endpoints - Distribute load across endpoint categories

Next Steps