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.
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:
| Endpoint | API Key | OAuth | JWT | Window |
|---|---|---|---|---|
POST /api/v1/documents/generate | 100 | 50 | 100 | 60 seconds |
POST /api/v1/documents/generate/batch | 10 | 5 | 10 | 60 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
| Endpoint | API Key | OAuth | JWT | Window |
|---|---|---|---|---|
GET /api/v1/documents/jobs | 300 | 150 | 300 | 60 seconds |
GET /api/v1/documents/jobs/:id | 300 | 150 | 300 | 60 seconds |
GET /api/v1/templates/* | 300 | 150 | 300 | 60 seconds |
Why 300/minute? Read operations are lightweight and used frequently in dashboards/reporting.
Default Limits (Other Endpoints)
| Authentication | Rate Limit | Window |
|---|---|---|
| API Key | 1000 | 60 seconds |
| OAuth | 500 | 60 seconds |
| JWT (Dashboard) | 500 | 60 seconds |
| Unauthenticated | 60 | 60 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:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Maximum requests allowed in window | 100 |
X-RateLimit-Remaining | Requests remaining in current window | 85 |
X-RateLimit-Reset | UTC timestamp when window resets | 2025-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:
| Header | Description | Example |
|---|---|---|
Retry-After | Seconds until you can retry | 45 |
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: Always429for rate limit errorscode:ERR_QUOTA_003(rate limit error code)message: Human-readable error messagerelatedInfo.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:
| Operation | API Key | OAuth | JWT |
|---|---|---|---|
| Document Generate | 100/min | 50/min | 100/min |
| Batch Generate | 10/min | 5/min | 10/min |
| Query Jobs | 300/min | 150/min | 300/min |
| Other Endpoints | 1000/min | 500/min | 500/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?
- Request is rejected with
429 Too Many Requests - Rate limit headers show
X-RateLimit-Remaining: 0 Retry-Afterheader tells you how long to wait- Window continues to slide - oldest requests expire
- 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-Resetshows 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:
- Sending too fast (< 2 seconds between requests)
- Multiple team members/API keys sending simultaneously
- Retry logic not implemented correctly
- 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-Resetheader 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
- Generating Documents: Learn how to generate documents in Generating Documents
- Error Handling: Handle all API errors in Error Handling
- Best Practices: Optimize your integration in Best Practices
Related: Generating Documents | Error Handling | Best Practices