Skip to main content

Rate Limiting

Understand API rate limits, headers, and how to handle rate limit errors.

Overview

RenderDoc uses rate limiting to ensure fair usage and protect API performance. Rate limits are applied per team and use a sliding window algorithm for accurate tracking.

info

Important: Rate limits are enforced automatically on all API endpoints. Exceeding limits returns a 429 Too Many Requests error with retry information.


Rate Limits by Endpoint

RenderDoc applies different rate limits based on the operation type:

Document Generation Endpoints

Rate limits vary by authentication type:

EndpointAPI KeyOAuthJWTWindow
POST /api/v1/documents/generate1005010060 seconds
POST /api/v1/documents/generate/batch1051060 seconds

Why different limits? OAuth has lower limits because third-party apps share platform resources. API Keys have higher limits for direct integrations by paying customers.

Query Endpoints

EndpointAPI KeyOAuthJWTWindow
GET /api/v1/documents/jobs30015030060 seconds
GET /api/v1/documents/jobs/:id30015030060 seconds
GET /api/v1/templates/*30015030060 seconds

Why 300/minute? Read operations are lightweight and used frequently in dashboards/reporting.

Default Limits (Other Endpoints)

AuthenticationRate LimitWindow
API Key100060 seconds
OAuth50060 seconds
JWT (Dashboard)50060 seconds
Unauthenticated6060 seconds

How Rate Limiting Works

RenderDoc uses a sliding window algorithm with Redis for accurate, distributed rate limiting:

Sliding Window Algorithm

Window: 60 seconds
Limit: 100 requests (for document generate with API Key)

[Request 1] [Request 2] ... [Request 100] ← All allowed

[Request 101] ← Rate limit exceeded (429 error)

Wait for window to slide forward

[Request 1 expires after 60s] ← New slot available

Benefits over fixed windows:

  • No burst traffic at window boundaries
  • More accurate request counting
  • Automatic cleanup of old requests

Rate Limit Scope

Rate limits are applied per team:

Team A: 100 requests/min ✅
Team B: 100 requests/min ✅ (separate limit)

User 1 (Team A): Count towards Team A
User 2 (Team A): Count towards Team A (shared limit)

Key points:

  • All team members share the same rate limit
  • API keys count towards the team's limit
  • Each team has independent limits

Rate Limit Headers

Every API response includes rate limit information in headers:

Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 85
X-RateLimit-Reset: 2025-11-07T10:31:00.000Z
Content-Type: application/json

Header definitions:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed in window100
X-RateLimit-RemainingRequests remaining in current window85
X-RateLimit-ResetUTC timestamp when window resets2025-11-07T10:31:00.000Z

When Rate Limit Exceeded

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2025-11-07T10:31:00.000Z
Retry-After: 45
Content-Type: application/json

Additional header when exceeded:

HeaderDescriptionExample
Retry-AfterSeconds until you can retry45

Error Response Format

When you exceed the rate limit, you'll receive a 429 error:

Rate Limit Exceeded Response

{
"statusCode": 429,
"code": "ERR_QUOTA_003",
"message": "Rate limit exceeded",
"timestamp": "2025-11-07T10:30:00.000Z",
"path": "/api/v1/documents/generate",
"relatedInfo": {
"limit": 100,
"windowSeconds": 60,
"resetAt": "2025-11-07T10:31:00.000Z",
"retryAfterSeconds": 45
}
}

Response fields:

  • statusCode: Always 429 for rate limit errors
  • code: ERR_QUOTA_003 (rate limit error code)
  • message: Human-readable error message
  • relatedInfo.limit: Your rate limit (requests per window)
  • relatedInfo.windowSeconds: Window duration (60 seconds)
  • relatedInfo.resetAt: When the window resets (UTC)
  • relatedInfo.retryAfterSeconds: How long to wait before retrying

Handling Rate Limits

Best Practices

1. Check Headers Proactively

Monitor rate limit headers in every response:

const response = await fetch('https://api.renderdoc.dev/api/v1/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ... })
});

// Check headers
const limit = parseInt(response.headers.get('X-RateLimit-Limit'));
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const reset = new Date(response.headers.get('X-RateLimit-Reset'));

console.log(`Rate limit: ${remaining}/${limit} remaining`);
console.log(`Resets at: ${reset.toLocaleString()}`);

// Slow down if approaching limit
if (remaining < 5) {
console.warn('Approaching rate limit! Slowing down...');
await sleep(2000); // Wait 2 seconds before next request
}

2. Implement Exponential Backoff

Retry with increasing delays when you hit the limit:

async function generateDocumentWithRetry(docData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.renderdoc.dev/api/v1/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(docData)
});

if (response.status === 429) {
// Rate limit exceeded
const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
const delay = Math.min(retryAfter * 1000, 2 ** attempt * 1000);

console.log(`Rate limited. Retrying in ${delay}ms...`);
await sleep(delay);
continue; // Retry
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

return await response.json(); // Success
} catch (error) {
if (attempt === maxRetries - 1) throw error; // Last attempt failed

// Exponential backoff for other errors
const delay = 2 ** attempt * 1000;
console.log(`Error: ${error.message}. Retrying in ${delay}ms...`);
await sleep(delay);
}
}
}

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

3. Use Retry-After Header

Always respect the Retry-After header value:

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

// Wait exactly as long as the server specifies
console.log(`Waiting ${retryAfter} seconds before retry...`);
await sleep(retryAfter * 1000);

// Retry request
return generateDocument(docData);
}

4. Implement Request Queuing

For high-volume applications, use a queue to control request rate:

class RateLimitedQueue {
constructor(requestsPerMinute = 100) {
this.requestsPerMinute = requestsPerMinute;
this.queue = [];
this.requestTimes = [];
}

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

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

this.processing = true;

while (this.queue.length > 0) {
const now = Date.now();
const oneMinuteAgo = now - 60000;

// Remove requests older than 1 minute
this.requestTimes = this.requestTimes.filter(time => time > oneMinuteAgo);

// Check if we can make another request
if (this.requestTimes.length >= this.requestsPerMinute) {
// Wait until oldest request is >1 minute old
const oldestRequest = this.requestTimes[0];
const waitTime = 60000 - (now - oldestRequest);
await sleep(waitTime);
continue;
}

// Process next request
const { requestFn, resolve, reject } = this.queue.shift();
this.requestTimes.push(now);

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

this.processing = false;
}
}

// Usage
const queue = new RateLimitedQueue(100); // 100 requests per minute

// Add requests to queue
const result1 = await queue.enqueue(() => generateDocument(doc1));
const result2 = await queue.enqueue(() => generateDocument(doc2));
// ... Queue automatically manages rate limiting

5. Batch Operations

Use bulk endpoints instead of individual requests:

❌ Bad - 100 individual requests:

for (const doc of documents) {
await generateDocument(doc); // 100 API calls
}

✅ Good - 1 bulk request:

await generateBatch(documents); // 1 API call

Rate Limit Summary

Rate limits are based on authentication type, not subscription plans:

OperationAPI KeyOAuthJWT
Document Generate100/min50/min100/min
Batch Generate10/min5/min10/min
Query Jobs300/min150/min300/min
Other Endpoints1000/min500/min500/min

Note: All teams have the same rate limits. For higher limits, contact support for enterprise solutions.


Code Examples

JavaScript/Node.js

Basic Rate Limit Checking

const apiKey = process.env.RENDERDOC_API_KEY;

async function generateDocument(docData) {
const response = await fetch('https://api.renderdoc.dev/api/v1/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(docData)
});

// Check rate limit headers
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');

console.log(`Rate limit: ${remaining}/${limit} remaining (resets at ${reset})`);

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`);
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

return response.json();
}

With Automatic Retry

async function generateDocumentWithRetry(docData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await generateDocument(docData);
} catch (error) {
if (error.message.includes('Rate limit exceeded') && attempt < maxRetries - 1) {
const match = error.message.match(/Retry after (\d+) seconds/);
const retryAfter = match ? parseInt(match[1]) : 60;

console.log(`Attempt ${attempt + 1} failed. Retrying in ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue;
}
throw error; // Rethrow if not rate limit or last attempt
}
}
}

Python

Basic Rate Limit Checking

import requests
import time
from datetime import datetime

API_KEY = os.environ['RENDERDOC_API_KEY']
BASE_URL = 'https://api.renderdoc.dev'

def generate_document(doc_data):
response = requests.post(
f'{BASE_URL}/api/v1/documents/generate',
headers={
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
json=doc_data
)

# Check rate limit headers
limit = int(response.headers.get('X-RateLimit-Limit', 0))
remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
reset = response.headers.get('X-RateLimit-Reset')

print(f'Rate limit: {remaining}/{limit} remaining (resets at {reset})')

if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
raise Exception(f'Rate limit exceeded. Retry after {retry_after} seconds.')

response.raise_for_status()
return response.json()

With Automatic Retry

def generate_document_with_retry(doc_data, max_retries=3):
for attempt in range(max_retries):
try:
return generate_document(doc_data)
except Exception as error:
if 'Rate limit exceeded' in str(error) and attempt < max_retries - 1:
# Extract retry_after from error message
import re
match = re.search(r'Retry after (\d+) seconds', str(error))
retry_after = int(match.group(1)) if match else 60

print(f'Attempt {attempt + 1} failed. Retrying in {retry_after}s...')
time.sleep(retry_after)
continue
raise # Rethrow if not rate limit or last attempt

PHP

<?php
$apiKey = getenv('RENDERDOC_API_KEY');
$baseUrl = 'https://api.renderdoc.dev';

function generateDocument($docData) {
global $apiKey, $baseUrl;

$ch = curl_init("$baseUrl/api/v1/documents/generate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($docData));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $apiKey",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_HEADER, true); // Include headers in output

$response = curl_exec($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

// Parse rate limit headers
preg_match('/X-RateLimit-Limit: (\d+)/', $headers, $limitMatch);
preg_match('/X-RateLimit-Remaining: (\d+)/', $headers, $remainingMatch);
preg_match('/X-RateLimit-Reset: (.+)/', $headers, $resetMatch);

$limit = $limitMatch[1] ?? 0;
$remaining = $remainingMatch[1] ?? 0;
$reset = $resetMatch[1] ?? 'unknown';

echo "Rate limit: $remaining/$limit remaining (resets at $reset)\n";

if ($statusCode === 429) {
preg_match('/Retry-After: (\d+)/', $headers, $retryAfterMatch);
$retryAfter = $retryAfterMatch[1] ?? 60;
throw new Exception("Rate limit exceeded. Retry after $retryAfter seconds.");
}

if ($statusCode >= 400) {
throw new Exception("HTTP $statusCode: $body");
}

return json_decode($body, true);
}

function generateDocumentWithRetry($docData, $maxRetries = 3) {
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
try {
return generateDocument($docData);
} catch (Exception $error) {
if (strpos($error->getMessage(), 'Rate limit exceeded') !== false &&
$attempt < $maxRetries - 1) {
preg_match('/Retry after (\d+) seconds/', $error->getMessage(), $match);
$retryAfter = $match[1] ?? 60;

echo "Attempt " . ($attempt + 1) . " failed. Retrying in {$retryAfter}s...\n";
sleep($retryAfter);
continue;
}
throw $error; // Rethrow if not rate limit or last attempt
}
}
}
?>

Testing Rate Limits

Manual Testing

Test rate limits using curl:

#!/bin/bash

API_KEY="your-api-key"
TEMPLATE_ID="your-template-id"

echo "Sending 105 requests (rate limit is 100/min)..."

for i in {1..105}; do
echo "Request $i:"

curl -X POST https://api.renderdoc.dev/api/v1/documents/generate \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "'$TEMPLATE_ID'",
"format": "pdf",
"variables": {}
}' \
-i | grep -E "HTTP|X-RateLimit|Retry-After"

echo "---"
done

echo "First 100 should succeed, last 5 should get 429 errors"

Expected output:

Request 1:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
---
...
Request 100:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
---
Request 101:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Retry-After: 45
---

Common Questions

How are rate limits calculated?

Per team using a sliding 60-second window:

  • Window tracks the last 60 seconds of requests
  • Limit is checked on each request
  • Old requests automatically drop out of the window
  • More accurate than fixed windows

What happens if I exceed the rate limit?

  1. Request is rejected with 429 Too Many Requests
  2. Rate limit headers show X-RateLimit-Remaining: 0
  3. Retry-After header tells you how long to wait
  4. Window continues to slide - oldest requests expire
  5. When window has available slots, requests succeed again

Do rate limits reset at a fixed time?

No. Rate limits use a sliding window, not a fixed window:

  • No specific "reset time"
  • X-RateLimit-Reset shows when the current window ends
  • Requests continuously expire after 60 seconds
  • No burst traffic at window boundaries

Can I request higher rate limits?

Yes, custom rate limits are available:

  • Contact us for high-volume requirements
  • Rate limits can be increased per team
  • Custom limits available for large-scale senders

Do failed requests count towards the limit?

Yes. All requests count, regardless of outcome:

  • ✅ Successful requests (200) - count
  • ❌ Failed requests (400, 500) - count
  • ⏸️ Rate limited requests (429) - count

Why? To prevent abuse and ensure fair usage.

Are rate limits per API key or per team?

Per team. All users and API keys in a team share the same limit:

  • User A + User B + API Key 1 = shared 100 requests/min (for document generate)
  • Exceeding limit affects all team members
  • Consider creating separate teams for independent limits

How do I monitor my rate limit usage?

Check headers on every response:

const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const limit = parseInt(response.headers.get('X-RateLimit-Limit'));
const percentUsed = ((limit - remaining) / limit) * 100;

if (percentUsed > 80) {
console.warn(`Using ${percentUsed}% of rate limit!`);
}

Troubleshooting

Getting 429 errors frequently?

Possible causes:

  1. Sending too fast (< 2 seconds between requests)
  2. Multiple team members/API keys sending simultaneously
  3. Retry logic not implemented correctly
  4. Using individual requests instead of bulk endpoints

Solutions:

  • Implement exponential backoff (see examples above)
  • Use bulk endpoints for multiple documents
  • Add delays between requests (2-3 seconds)
  • Implement request queuing
  • Upgrade to higher plan (future)

Retry-After header missing?

If Retry-After header is missing:

  • Default to 60 seconds
  • Use exponential backoff (2^attempt seconds)
  • Check X-RateLimit-Reset header for exact reset time

Rate limit headers not appearing?

All endpoints include rate limit headers. If missing:

  • Check endpoint URL (must be /api/v1/documents/...)
  • Verify authentication (rate limits require valid auth)
  • Contact support if issue persists

Next Steps


Related: Generating Documents | Error Handling | Best Practices