My App
DocExtract API

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:

PlanRequests per MinuteRequests per HourConcurrent Jobs
Free101002
Starter601,0005
Professional1003,00010
Business50015,00050
EnterpriseCustomCustomCustom

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
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix 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: 45

The 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:

PlanMax Concurrent Jobs
Free2
Starter5
Professional10
Business50
EnterpriseCustom

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 request

Cache 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

  1. Monitor rate limit headers: Check headers on every response
  2. Implement retry logic: Use exponential backoff
  3. Use webhooks: Avoid polling when possible
  4. Cache static data: Reduce redundant requests
  5. Queue requests: Process documents in batches
  6. Set up alerts: Monitor for rate limit issues
  7. 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.

Next Steps