Skip to main content

Rate Limiting

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

Overview​

Rynko 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​

Rynko 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​

Rynko 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.rynko.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.rynko.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.RYNKO_API_KEY;

async function generateDocument(docData) {
const response = await fetch('https://api.rynko.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['RYNKO_API_KEY']
BASE_URL = 'https://api.rynko.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('RYNKO_API_KEY');
$baseUrl = 'https://api.rynko.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.rynko.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