Rate Limits

API usage limits, tier details, and best practices for efficient API consumption.

Overview

The HOLE Foundation API implements rate limiting to ensure fair usage and service reliability for all users. Limits are applied per authentication token and reset on a rolling window basis.

Rate Limit Tiers

Free Tier

Perfect for experimentation and small projects.

Endpoint CategoryRequests/DayRequests/Hour
HealthUnlimitedUnlimited
Vector Search10020
Transparency10020
FOIA Dashboard5010
Chat205

Cost: Free Signup: Get started

Standard Tier

For production applications and regular use.

Endpoint CategoryRequests/DayRequests/Hour
HealthUnlimitedUnlimited
Vector Search1,000200
Transparency1,000200
FOIA Dashboard500100
Chat20040

Cost: $29/month Upgrade: Contact sales

Premium Tier

For high-volume applications and enterprise use.

Endpoint CategoryRequests/DayRequests/Hour
HealthUnlimitedUnlimited
Vector Search10,0002,000
Transparency10,0002,000
FOIA DashboardUnlimited1,000
Chat2,000400

Cost: $199/month Custom: Enterprise pricing available

Enterprise Tier

Custom limits and SLA for mission-critical applications.

  • Custom rate limits
  • Dedicated support
  • SLA guarantees
  • Priority access
  • Custom integrations

Contact: Enterprise sales

Rate Limit Headers

Every API response includes rate limit information:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1705320000

Header Descriptions

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in current window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when limit resets

Example Response

1HTTP/1.1 200 OK
2Content-Type: application/json
3X-RateLimit-Limit: 100
4X-RateLimit-Remaining: 87
5X-RateLimit-Reset: 1705320000
6
7{
8 "results": [...]
9}

Rate Limit Exceeded

When you exceed your rate limit, you’ll receive a 429 Too Many Requests response:

1{
2 "error": {
3 "code": "RATE_LIMIT_EXCEEDED",
4 "message": "Rate limit exceeded. Try again in 60 seconds",
5 "status": 429,
6 "details": {
7 "limit": 100,
8 "window": "1 hour",
9 "reset_at": "2024-01-15T11:00:00Z"
10 }
11 }
12}

Response includes Retry-After header:

1HTTP/1.1 429 Too Many Requests
2Retry-After: 60
3X-RateLimit-Limit: 100
4X-RateLimit-Remaining: 0
5X-RateLimit-Reset: 1705320000

Handling Rate Limits

TypeScript

Check rate limit headers:

1import { HoleFoundationClient } from '@hole-foundation/sdk';
2
3const client = new HoleFoundationClient({
4 environment: 'https://api.theholefoundation.org',
5 token: 'your-jwt-token'
6});
7
8const response = await client.vectorSearch.search({
9 query: 'FOIA exemptions'
10});
11
12// Access rate limit info from headers
13console.log('Remaining requests:', response.headers['x-ratelimit-remaining']);
14console.log('Limit resets at:', new Date(
15 parseInt(response.headers['x-ratelimit-reset']) * 1000
16));

Automatic Retry

1async function searchWithRetry(query: string, maxRetries = 3) {
2 for (let i = 0; i < maxRetries; i++) {
3 try {
4 return await client.vectorSearch.search({ query });
5 } catch (error) {
6 if (error instanceof TheholetruthApiError &&
7 error.status === 429) {
8 // Get retry-after from error
9 const resetAt = new Date(error.body.error.details.reset_at);
10 const waitMs = resetAt.getTime() - Date.now();
11
12 console.log(`Rate limited. Waiting ${waitMs}ms...`);
13 await new Promise(resolve => setTimeout(resolve, waitMs));
14 continue;
15 }
16 throw error;
17 }
18 }
19 throw new Error('Max retries exceeded');
20}
21
22// Usage
23const results = await searchWithRetry('FOIA exemptions');

Python

1from hole_foundation_api import HoleFoundationClient
2from hole_foundation_api.errors import TheholetruthApiError
3import time
4
5client = HoleFoundationClient(
6 environment="https://api.theholefoundation.org",
7 token="your-jwt-token"
8)
9
10def search_with_retry(query, max_retries=3):
11 for i in range(max_retries):
12 try:
13 return client.vector_search.search(query=query)
14 except TheholetruthApiError as error:
15 if error.status_code == 429:
16 # Get reset time
17 reset_at = error.body['error']['details']['reset_at']
18 from datetime import datetime
19 wait_seconds = (
20 datetime.fromisoformat(reset_at.replace('Z', '+00:00')) -
21 datetime.now()
22 ).total_seconds()
23
24 print(f"Rate limited. Waiting {wait_seconds}s...")
25 time.sleep(wait_seconds)
26 continue
27 raise
28 raise Exception("Max retries exceeded")
29
30# Usage
31results = search_with_retry("FOIA exemptions")

Best Practices

1. Cache Responses

Reduce API calls by caching common queries:

1class CachedClient {
2 private cache = new Map<string, any>();
3 private client: HoleFoundationClient;
4
5 constructor(client: HoleFoundationClient) {
6 this.client = client;
7 }
8
9 async search(query: string) {
10 // Check cache
11 if (this.cache.has(query)) {
12 return this.cache.get(query);
13 }
14
15 // Call API
16 const results = await this.client.vectorSearch.search({ query });
17
18 // Cache for 1 hour
19 this.cache.set(query, results);
20 setTimeout(() => this.cache.delete(query), 60 * 60 * 1000);
21
22 return results;
23 }
24}
25
26const cachedClient = new CachedClient(client);
27
28// First call hits API
29const results1 = await cachedClient.search('FOIA');
30
31// Second call uses cache
32const results2 = await cachedClient.search('FOIA');

2. Batch Requests

Combine multiple queries when possible:

1// ❌ Multiple sequential calls
2const results1 = await client.vectorSearch.search({ query: 'FOIA' });
3const results2 = await client.vectorSearch.search({ query: 'privacy' });
4const results3 = await client.vectorSearch.search({ query: 'exemptions' });
5
6// ✅ Single call with broader query, filter client-side
7const allResults = await client.vectorSearch.search({
8 query: 'FOIA privacy exemptions',
9 limit: 100
10});
11
12// Filter results locally
13const foiaResults = allResults.results.filter(r =>
14 r.title.includes('FOIA')
15);
16const privacyResults = allResults.results.filter(r =>
17 r.title.includes('privacy')
18);

3. Monitor Usage

Track your rate limit usage:

1class RateLimitMonitor {
2 private remaining = 0;
3 private limit = 0;
4 private resetAt = 0;
5
6 async search(client: HoleFoundationClient, query: string) {
7 const response = await client.vectorSearch.search({ query });
8
9 // Update from headers
10 this.remaining = parseInt(
11 response.headers['x-ratelimit-remaining']
12 );
13 this.limit = parseInt(
14 response.headers['x-ratelimit-limit']
15 );
16 this.resetAt = parseInt(
17 response.headers['x-ratelimit-reset']
18 );
19
20 // Warn if low
21 if (this.remaining < this.limit * 0.1) {
22 console.warn(
23 `⚠️ Low on requests: ${this.remaining}/${this.limit} remaining`
24 );
25 }
26
27 return response;
28 }
29
30 getStatus() {
31 const resetDate = new Date(this.resetAt * 1000);
32 return {
33 remaining: this.remaining,
34 limit: this.limit,
35 resetAt: resetDate,
36 percentUsed: ((this.limit - this.remaining) / this.limit) * 100
37 };
38 }
39}
40
41const monitor = new RateLimitMonitor();
42const results = await monitor.search(client, 'FOIA');
43console.log('Rate limit status:', monitor.getStatus());

4. Request Prioritization

Prioritize important requests:

1class PriorityQueue {
2 private queue: Array<{
3 fn: () => Promise<any>;
4 priority: number;
5 }> = [];
6
7 private processing = false;
8
9 async add(fn: () => Promise<any>, priority = 0) {
10 return new Promise((resolve, reject) => {
11 this.queue.push({
12 fn: async () => {
13 try {
14 const result = await fn();
15 resolve(result);
16 } catch (error) {
17 reject(error);
18 }
19 },
20 priority
21 });
22
23 // Sort by priority (higher first)
24 this.queue.sort((a, b) => b.priority - a.priority);
25
26 this.process();
27 });
28 }
29
30 private async process() {
31 if (this.processing || this.queue.length === 0) return;
32
33 this.processing = true;
34
35 while (this.queue.length > 0) {
36 const item = this.queue.shift()!;
37 await item.fn();
38
39 // Small delay between requests
40 await new Promise(resolve => setTimeout(resolve, 100));
41 }
42
43 this.processing = false;
44 }
45}
46
47const queue = new PriorityQueue();
48
49// High priority
50await queue.add(
51 () => client.vectorSearch.search({ query: 'urgent FOIA' }),
52 10
53);
54
55// Low priority
56await queue.add(
57 () => client.vectorSearch.search({ query: 'background research' }),
58 1
59);

5. Implement Circuit Breaker

Prevent repeated failures:

1class CircuitBreaker {
2 private failures = 0;
3 private threshold = 3;
4 private timeout = 60000; // 1 minute
5 private state: 'closed' | 'open' | 'half-open' = 'closed';
6 private nextAttempt = 0;
7
8 async execute<T>(fn: () => Promise<T>): Promise<T> {
9 if (this.state === 'open') {
10 if (Date.now() < this.nextAttempt) {
11 throw new Error('Circuit breaker is open');
12 }
13 this.state = 'half-open';
14 }
15
16 try {
17 const result = await fn();
18
19 // Success - reset
20 if (this.state === 'half-open') {
21 this.state = 'closed';
22 this.failures = 0;
23 }
24
25 return result;
26 } catch (error) {
27 this.failures++;
28
29 if (this.failures >= this.threshold) {
30 this.state = 'open';
31 this.nextAttempt = Date.now() + this.timeout;
32 console.log('Circuit breaker opened');
33 }
34
35 throw error;
36 }
37 }
38}
39
40const breaker = new CircuitBreaker();
41
42// Usage
43try {
44 const results = await breaker.execute(() =>
45 client.vectorSearch.search({ query: 'FOIA' })
46 );
47} catch (error) {
48 console.error('Request failed or circuit is open');
49}

Rate Limit Exemptions

Some operations don’t count against rate limits:

  • Health check endpoint (/health)
  • Authentication/token validation
  • Rate limit status queries
  • Error responses (4xx, 5xx)

Upgrading Your Tier

Check Current Usage

View your current tier and usage in the dashboard:

1const usage = await client.account.getUsage();
2
3console.log('Current tier:', usage.tier);
4console.log('Requests this month:', usage.requests_this_month);
5console.log('Requests today:', usage.requests_today);
6console.log('Recommendation:', usage.tier_recommendation);

Contact Sales

To upgrade your tier:

  1. Review your usage patterns
  2. Estimate future needs
  3. Contact sales: api@theholefoundation.org
  4. Upgrade effective immediately

Common Questions

Do failed requests count?

  • Successful requests (2xx): Count against limit
  • Client errors (4xx): Count against limit
  • Server errors (5xx): Don’t count against limit
  • Rate limit errors (429): Don’t count against limit

When do limits reset?

Rate limits reset on a rolling window basis. For example, if you use 50 requests at 10:00 AM, those 50 “credits” become available again at 11:00 AM.

Can I increase my limits temporarily?

Yes! For special events or temporary high-volume needs:

  1. Email api@theholefoundation.org
  2. Explain your use case
  3. Specify duration needed
  4. Temporary increase granted within 24 hours

What happens if I exceed limits?

  1. You’ll receive a 429 error
  2. Additional requests blocked until reset
  3. No charges or penalties
  4. Suggested to upgrade tier if consistent

Support

Questions about rate limits?