Rate Limiting

Rate limiting protects both your application and the Archiva service by preventing excessive requests that could overwhelm the system or cause issues for other users. Think of it like a speed limit on a highway—it ensures everyone can use the service reliably.

Understanding Rate Limits

Rate limits prevent any single API key from overwhelming the service with too many requests. This ensures:

  • Fair usage: All customers get reliable service
  • System stability: Prevents one customer's traffic from affecting others
  • Cost control: Prevents accidental runaway processes from creating excessive costs

Current Limits

  • Limit: 100 requests per 60 seconds per API key
  • Window: 60-second rolling window (not a fixed minute)
  • Scope: Each API key has its own independent counter

What this means: You can make up to 100 requests in any 60-second period. If you make 100 requests at 10:00:00, you'll need to wait until 10:01:00 before making more (though you can make requests as soon as the oldest ones fall outside the 60-second window).

Rate limits are currently applied to the Create Event endpoint (POST /api/ingest/event). The List Events endpoint does not currently enforce rate limiting, but this may change in the future.

Rate Limit Headers

The API does not currently return rate limit information in response headers. When you exceed the rate limit, you'll receive a 429 Too Many Requests response.

Exceeding Rate Limits

When you exceed the rate limit, you'll receive:

Status Code: 429 Too Many Requests

Response Body:

{
  "error": "Rate limit exceeded"
}

Response Headers:

Retry-After: 45

The Retry-After header indicates the number of seconds to wait before retrying the request. This value represents the time remaining in the current rate limit window.

Handling Rate Limits

Strategy 1: Exponential Backoff

Implement exponential backoff when you receive a 429 response:

async function createEventWithRetry(data, apiKey, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch('https://api.archiva.app/api/ingest/event', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      });

      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
        const waitTime = retryAfter * 1000; // Convert to milliseconds
        await new Promise(resolve => setTimeout(resolve, waitTime));
        continue;
      }

      return response;
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      // Exponential backoff on other errors
      await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
    }
  }
}

Strategy 2: Batching

Instead of sending events one at a time, batch multiple events into a single request if the API supports bulk ingestion in the future. For now, ensure you stay within the rate limit for individual requests.

Strategy 3: Request Queuing

Implement a request queue to smooth out request bursts:

class RateLimitedQueue {
  constructor(apiKey, requestsPerMinute = 100) {
    this.apiKey = apiKey;
    this.requestsPerMinute = requestsPerMinute;
    this.queue = [];
    this.processing = false;
    this.requestTimes = [];
  }

  async add(eventData) {
    return new Promise((resolve, reject) => {
      this.queue.push({ eventData, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      // Remove requests older than 1 minute
      const now = Date.now();
      this.requestTimes = this.requestTimes.filter(
        time => now - time < 60000
      );

      // Wait if we're at the rate limit
      if (this.requestTimes.length >= this.requestsPerMinute) {
        const oldestRequest = this.requestTimes[0];
        const waitTime = 60000 - (now - oldestRequest);
        await new Promise(resolve => setTimeout(resolve, waitTime));
        continue;
      }

      // Process next request
      const { eventData, resolve, reject } = this.queue.shift();
      this.requestTimes.push(Date.now());

      try {
        const response = await fetch('https://api.archiva.app/api/ingest/event', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(eventData),
        });

        if (response.status === 429) {
          // Re-queue the request
          this.queue.unshift({ eventData, resolve, reject });
          const retryAfter = parseInt(
            response.headers.get('Retry-After') || '60',
            10
          );
          await new Promise(resolve =>
            setTimeout(resolve, retryAfter * 1000)
          );
        } else {
          resolve(response);
        }
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

Best Practices

  1. Monitor rate limit responses: Track 429 responses in your monitoring system
  2. Implement retry logic: Always handle rate limit errors gracefully
  3. Respect Retry-After: Use the Retry-After header to determine when to retry
  4. Use multiple API keys: If you have multiple projects, distribute load across different API keys
  5. Batch when possible: If the API adds bulk ingestion, use it to reduce request count

Testing Rate Limits

To test rate limiting behavior, you can send multiple requests rapidly:

for i in {1..150}; do
  curl -X POST https://api.archiva.app/api/ingest/event \
    -H "Authorization: Bearer pk_test_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "actionKey": "test.rate_limit",
      "entityType": "test",
      "entityId": "test_'$i'",
      "actorType": "user",
      "actorId": "test_actor"
    }' &
done
wait

You should see 429 responses after the first 100 requests.