Skip to content

Rate Limiting

Quota management is critical for bibliographic APIs. Each package implements uniform rate limiting handling.

Limits by Source

SourceLimitHeaders
OpenAlex100k/day (with API key)X-Rate-Limit-*
Crossref50 req/s (polite pool with email)X-Rate-Limit-Limit, X-Rate-Limit-Interval
HALUndocumented-
ArXiv~1 req/3s recommended-
ORCIDVariable by tierX-RateLimit-*

Typed Errors

typescript
import { Data } from 'effect';

// Rate limit error (retryable)
export class RateLimitError extends Data.TaggedError('RateLimitError')<{
  readonly source: string;
  readonly retryAfter?: number;      // Seconds before retry
  readonly remaining?: number;        // Remaining requests
  readonly limit?: number;            // Total limit
  readonly resetAt?: Date;            // Quota reset time
}> {}

// Daily quota exhausted (non-retryable)
export class QuotaExhaustedError extends Data.TaggedError('QuotaExhaustedError')<{
  readonly source: string;
  readonly dailyLimit: number;
  readonly resetAt: Date;
}> {}

Common Interface

All clients implement RateLimitedClient:

typescript
interface RateLimitStatus {
  source: string;
  remaining: number;
  limit: number;
  resetAt: Date;
  usedToday: number;
}

interface RateLimitedClient {
  // Get current quota status
  getRateLimitStatus: () => Effect<RateLimitStatus, never>;

  // Wait for quota to become available
  waitForQuota: () => Effect<void, QuotaExhaustedError>;
}

Retry Strategy

Retry is automatic with exponential backoff and jitter:

typescript
import { Schedule, pipe } from 'effect';

const rateLimitRetrySchedule = pipe(
  Schedule.exponential('1 second'),
  Schedule.jittered,
  Schedule.whileInput<RateLimitError>((e) => e.retryAfter !== undefined),
  Schedule.compose(Schedule.recurs(5))
);

Usage

Check quotas

typescript
import { createOpenAlexClient } from '@univ-lehavre/atlas-openalex';

const client = createOpenAlexClient({ apiKey: '...' });

// Get status
const status = yield* client.getRateLimitStatus();
console.log(`Remaining: ${status.remaining}/${status.limit}`);
console.log(`Reset at: ${status.resetAt}`);

Handle quota errors

typescript
import { Effect, Match } from 'effect';

const program = pipe(
  client.listWorks({ perPage: 100 }),
  Effect.catchTag('RateLimitError', (error) =>
    Effect.gen(function* () {
      console.log(`Rate limited, waiting ${error.retryAfter}s...`);
      yield* Effect.sleep(`${error.retryAfter} seconds`);
      return yield* client.listWorks({ perPage: 100 });
    })
  ),
  Effect.catchTag('QuotaExhaustedError', (error) =>
    Effect.fail(new Error(`Daily quota exhausted. Resets at ${error.resetAt}`))
  )
);

Wait for quota

typescript
// Wait for quota to become available
yield* client.waitForQuota();

// Then execute the request
const works = yield* client.listWorks();

Crossref: Polite Pool

Crossref offers a better rate limit if you include an email in your requests:

typescript
const client = createCrossrefClient({
  mailto: 'your-email@example.com',  // Activates the "polite pool"
});

With the polite pool:

  • Increased limit
  • Priority in the queue
  • Notifications in case of issues

OpenAlex: API Key

OpenAlex offers 100k requests/day with a free API key:

typescript
const client = createOpenAlexClient({
  apiKey: process.env.OPENALEX_API_KEY,
});

Without an API key, the limit is IP-based and more restrictive.

Monitoring in atlas-citations

The aggregator allows monitoring all quotas:

typescript
import { createCitationsClient } from '@univ-lehavre/atlas-citations';

const client = createCitationsClient();

// Get quotas for all sources
const limits = yield* client.getRateLimits();
// {
//   openalex: { remaining: 99500, limit: 100000, resetAt: ... },
//   crossref: { remaining: 45, limit: 50, resetAt: ... },
//   hal: { remaining: null, limit: null, resetAt: null },
//   ...
// }

// Check source health
const health = yield* client.getSourceHealth();
// {
//   openalex: { status: 'healthy', latency: 120 },
//   crossref: { status: 'degraded', latency: 850 },
//   arxiv: { status: 'healthy', latency: 200 },
//   ...
// }