Rate Limits
Understand API rate limits and how to work within them
Rate Limits
The DocExtract API implements rate limiting to ensure fair usage and system stability. Rate limits are applied per organization and per API key.
Rate Limit Tiers
Rate limits vary by plan:
| Plan | Requests per Minute | Requests per Hour | Concurrent Jobs |
|---|---|---|---|
| Free | 10 | 100 | 2 |
| Starter | 60 | 1,000 | 5 |
| Professional | 100 | 3,000 | 10 |
| Business | 500 | 15,000 | 50 |
| Enterprise | Custom | Custom | Custom |
Need higher limits? Contact sales@adteco.com for Enterprise pricing with custom rate limits.
Rate Limit Headers
Every API response includes rate limit information in the headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1700000000| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when the limit resets |
Checking Rate Limits
Read rate limit headers from responses:
const response = await fetch('https://api.adteco.com/v1/extractors', {
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
},
});
// 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}`);
console.log(`Resets at: ${new Date(parseInt(reset!) * 1000).toISOString()}`);
const data = await response.json();response = requests.get(
'https://api.adteco.com/v1/extractors',
headers={'Authorization': 'Bearer sk_live_your_api_key'},
)
# Check rate limit headers
limit = response.headers.get('X-RateLimit-Limit')
remaining = response.headers.get('X-RateLimit-Remaining')
reset = response.headers.get('X-RateLimit-Reset')
print(f'Rate Limit: {remaining}/{limit}')
print(f'Resets at: {datetime.fromtimestamp(int(reset))}')
data = response.json()req, _ := http.NewRequest("GET", "https://api.adteco.com/v1/extractors", nil)
req.Header.Set("Authorization", "Bearer sk_live_your_api_key")
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// Check rate limit headers
limit := resp.Header.Get("X-RateLimit-Limit")
remaining := resp.Header.Get("X-RateLimit-Remaining")
reset := resp.Header.Get("X-RateLimit-Reset")
log.Printf("Rate Limit: %s/%s", remaining, limit)Rate Limit Exceeded
When you exceed the rate limit, the API returns a 429 Too Many Requests error:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Limit: 100 requests/minute",
"status": 429,
"retry_after": 45
}
}Response Headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000060
Retry-After: 45The Retry-After header tells you how many seconds to wait before retrying.
Handling Rate Limits
Exponential Backoff
Implement exponential backoff to handle rate limits gracefully:
async function callApiWithBackoff<T>(
apiCall: () => Promise<Response>,
maxRetries: number = 5
): Promise<T> {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await apiCall();
// Check for rate limiting
if (response.status === 429) {
const retryAfter = parseInt(
response.headers.get('Retry-After') || '60'
);
attempt++;
if (attempt >= maxRetries) {
throw new Error('Max retries exceeded');
}
// Wait for the specified time
console.log(`Rate limited. Waiting ${retryAfter}s before retry...`);
await sleep(retryAfter * 1000);
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response.json();
} catch (error) {
if (attempt >= maxRetries - 1) {
throw error;
}
// Exponential backoff for other errors
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
console.log(`Request failed. Retrying in ${delay}ms...`);
await sleep(delay);
attempt++;
}
}
throw new Error('Max retries exceeded');
}
// Usage
const data = await callApiWithBackoff(() =>
fetch('https://api.adteco.com/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
extractor_id: 'ext_abc123...',
document: base64Document,
mime_type: 'application/pdf',
}),
})
);Proactive Rate Limiting
Monitor rate limit headers and slow down before hitting the limit:
class RateLimitedApiClient {
private remaining: number = 100;
private limit: number = 100;
private resetTime: number = Date.now() + 60000;
async call(url: string, options: RequestInit): Promise<any> {
// Check if we're close to the limit
if (this.remaining < 5) {
const timeUntilReset = this.resetTime - Date.now();
if (timeUntilReset > 0) {
console.log(`Approaching rate limit. Waiting ${timeUntilReset}ms...`);
await sleep(timeUntilReset);
}
}
const response = await fetch(url, options);
// Update rate limit tracking
this.limit = parseInt(response.headers.get('X-RateLimit-Limit') || '100');
this.remaining = parseInt(
response.headers.get('X-RateLimit-Remaining') || '0'
);
this.resetTime =
parseInt(response.headers.get('X-RateLimit-Reset') || '0') * 1000;
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
await sleep(retryAfter * 1000);
return this.call(url, options); // Retry
}
return response.json();
}
}
// Usage
const client = new RateLimitedApiClient();
const results = await Promise.all(
documents.map(doc =>
client.call('https://api.adteco.com/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
extractor_id: 'ext_abc123...',
document: doc,
mime_type: 'application/pdf',
}),
})
)
);Request Queuing
Queue requests to stay within rate limits:
import PQueue from 'p-queue';
// Create queue with rate limiting
const queue = new PQueue({
intervalCap: 100, // 100 requests
interval: 60 * 1000, // per minute
concurrency: 10, // max 10 concurrent requests
});
// Process documents with automatic rate limiting
async function processDocuments(extractorId: string, documents: string[]) {
const results = await Promise.all(
documents.map(doc =>
queue.add(async () => {
const response = await fetch('https://api.adteco.com/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
extractor_id: extractorId,
document: doc,
mime_type: 'application/pdf',
}),
});
return response.json();
})
)
);
return results;
}
// Process 500 documents - automatically queued to respect rate limits
const results = await processDocuments('ext_abc123...', documents);Concurrent Job Limits
In addition to request rate limits, there are limits on concurrent processing jobs:
| Plan | Max Concurrent Jobs |
|---|---|
| Free | 2 |
| Starter | 5 |
| Professional | 10 |
| Business | 50 |
| Enterprise | Custom |
When you submit a job while at the concurrent limit, the new job will queue until a slot becomes available.
Monitor Concurrent Jobs
async function getActiveJobs() {
const response = await fetch(
'https://api.adteco.com/v1/documents?status=processing',
{
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
},
}
);
const data = await response.json();
return data.jobs.length;
}
// Before submitting many jobs
const activeJobs = await getActiveJobs();
const concurrentLimit = 10; // Your plan's limit
if (activeJobs >= concurrentLimit) {
console.log('At concurrent job limit. Waiting for jobs to complete...');
// Wait or implement queuing logic
}Optimization Strategies
Use Webhooks Instead of Polling
Polling wastes rate limit quota. Use webhooks for real-time notifications:
Bad (Polling):
// Uses up to 60 requests per minute
while (true) {
const job = await getJob(jobId);
if (job.status === 'completed') break;
await sleep(1000); // Poll every second
}Good (Webhooks):
// Uses 1 request to submit, webhook notifies when complete
const job = await submitDocument(extractorId, document);
// Webhook handler receives notification automatically
app.post('/webhooks/docextract', (req, res) => {
const event = req.body;
if (event.event === 'job.completed') {
handleCompletedJob(event.data);
}
res.status(200).send('OK');
});Batch Operations
Retrieve multiple resources in a single request:
// Instead of individual requests for each job
for (const jobId of jobIds) {
const job = await getJob(jobId); // 100 requests
}
// Batch query with filters
const jobs = await fetch(
'https://api.adteco.com/v1/documents?limit=100',
{
headers: { 'Authorization': 'Bearer sk_live_your_api_key' },
}
).then(r => r.json()); // 1 requestCache Responses
Cache static data to reduce API calls:
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 600 }); // 10 minute TTL
async function getExtractorCached(extractorId: string) {
// Check cache first
const cached = cache.get(extractorId);
if (cached) {
return cached;
}
// Fetch from API
const response = await fetch(
`https://api.adteco.com/v1/extractors/${extractorId}`,
{
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
},
}
);
const extractor = await response.json();
// Cache for future requests
cache.set(extractorId, extractor);
return extractor;
}Parallel Processing with Concurrency Limits
Process documents in parallel while respecting limits:
async function processBatchWithConcurrency(
extractorId: string,
documents: string[],
maxConcurrent: number = 10
) {
const queue: Promise<any>[] = [];
const results = [];
for (let i = 0; i < documents.length; i++) {
// Wait if at max concurrent
if (queue.length >= maxConcurrent) {
const completed = await Promise.race(queue);
results.push(completed);
queue.splice(queue.indexOf(completed), 1);
}
// Submit new job
const promise = fetch('https://api.adteco.com/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
extractor_id: extractorId,
document: documents[i],
mime_type: 'application/pdf',
}),
}).then(r => r.json());
queue.push(promise);
}
// Wait for remaining jobs
results.push(...(await Promise.all(queue)));
return results;
}Monitoring Rate Limit Usage
Track Usage Over Time
interface RateLimitMetrics {
timestamp: number;
limit: number;
remaining: number;
used: number;
}
const metrics: RateLimitMetrics[] = [];
function trackRateLimitMetrics(response: Response) {
const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '0');
const remaining = parseInt(
response.headers.get('X-RateLimit-Remaining') || '0'
);
metrics.push({
timestamp: Date.now(),
limit,
remaining,
used: limit - remaining,
});
// Keep last hour of metrics
const oneHourAgo = Date.now() - 60 * 60 * 1000;
while (metrics.length > 0 && metrics[0].timestamp < oneHourAgo) {
metrics.shift();
}
}
function getRateLimitStats() {
if (metrics.length === 0) return null;
const recent = metrics.slice(-10);
const avgUsage = recent.reduce((sum, m) => sum + m.used, 0) / recent.length;
const maxUsage = Math.max(...recent.map(m => m.used));
return {
average_usage: avgUsage,
max_usage: maxUsage,
current_remaining: recent[recent.length - 1].remaining,
};
}Alerts for Rate Limit Issues
function checkRateLimitHealth(response: Response) {
const remaining = parseInt(
response.headers.get('X-RateLimit-Remaining') || '0'
);
const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '100');
const percentRemaining = (remaining / limit) * 100;
// Alert if usage is very high
if (percentRemaining < 10) {
console.warn(
`⚠️ Rate limit usage is high: ${remaining}/${limit} remaining (${percentRemaining.toFixed(1)}%)`
);
// Send alert to monitoring service
sendAlert({
type: 'rate_limit_warning',
remaining,
limit,
percent_remaining: percentRemaining,
});
}
}Enterprise Rate Limits
Enterprise plans offer:
- Custom rate limits: Tailored to your volume
- Dedicated infrastructure: Isolated resources
- Priority processing: Faster queue priority
- Burst capacity: Handle traffic spikes
- SLA guarantees: Contractual uptime commitments
Contact sales@adteco.com to discuss Enterprise options.
Best Practices
- Monitor rate limit headers: Check headers on every response
- Implement retry logic: Use exponential backoff
- Use webhooks: Avoid polling when possible
- Cache static data: Reduce redundant requests
- Queue requests: Process documents in batches
- Set up alerts: Monitor for rate limit issues
- Plan for growth: Upgrade before hitting limits regularly
FAQs
Are rate limits per API key or per organization?
Rate limits are per organization. All API keys within an organization share the same rate limit.
Do test API keys have rate limits?
Yes, test keys have the same rate limits as live keys for your plan.
What happens if I exceed the rate limit?
Requests will fail with a 429 error. The Retry-After header indicates when you can retry.
Can I increase my rate limits?
Yes, upgrade to a higher plan or contact sales for Enterprise custom limits.
Do failed requests count toward the rate limit?
Yes, all requests (successful or failed) count toward the rate limit.
Is there a daily rate limit?
No, rate limits are per minute and per hour. There's no daily cap.