Skip to main content

Rate Limits

Limits protect the API and ensure fair usage.

Rate Limit Summaryโ€‹

Two Types of Limits

The API enforces both daily and per-minute limits. You must stay within both.

Daily Limits (Rolling 24-Hour Window)โ€‹

TierDaily CallsHow to Get
Anonymous100Default
SIWE Authenticated200Sign in with Ethereum
API Key500Purchase credits

Daily limits use a rolling 24-hour window โ€” each request expires exactly 24 hours after it was made.

Per-Minute Limits (Rolling 60-Second Window)โ€‹

TierRequests/MinuteBurst Buffer
Anonymous30โ€”
SIWE30โ€”
API Key60Higher burst allowed

Per-minute limits use a rolling 60-second window โ€” the limit is always based on requests made in the last 60 seconds.

Example: Even with 100 daily calls, you cannot make more than 30 requests in any 60-second window.

Upgrade Path

API Key holders get 60 requests/minute (2x the standard limit) in addition to 500 daily calls.

Rate Limit Trackingโ€‹

By IP vs By Identityโ€‹

TierTracking Method
AnonymousBy IP address
SIWE AuthenticatedBy wallet address
API KeyBy API key
IP Tracking

Anonymous rate limits are tracked per IP. If you're behind a shared NAT or proxy, you may share limits with other users.

Rate Limit Headersโ€‹

Every API response includes rate limit headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1707440400
Content-Type: application/json
HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed in period100
X-RateLimit-RemainingRequests remaining in current window73
X-RateLimit-ResetUnix timestamp when limit resets1707440400

Parsing Reset Timeโ€‹

const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
const resetDate = new Date(resetTime * 1000);
const secondsUntilReset = Math.ceil((resetTime * 1000 - Date.now()) / 1000);

console.log(`Rate limit resets at ${resetDate.toISOString()}`);
console.log(`Seconds until reset: ${secondsUntilReset}`);

Check Your Usageโ€‹

curl https://api.web3identity.com/api/usage
{
"used": 27,
"limit": 100,
"remaining": 73,
"resetsAt": "2026-02-09T00:00:00Z",
"tier": "anonymous",
"perMinute": {
"used": 5,
"limit": 30,
"remaining": 25
}
}

429 Too Many Requestsโ€‹

When rate limited, the API returns a 429 status with details:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1707440400
{
"error": true,
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Please wait before making more requests.",
"retryAfter": 60,
"limit": 100,
"remaining": 0,
"resetAt": "2026-02-09T00:00:00Z",
"limitType": "per_minute"
}

Response Fieldsโ€‹

FieldTypeDescription
retryAfternumberSeconds to wait before retrying
limitnumberYour rate limit
remainingnumberRequests remaining (always 0 when limited)
resetAtstringISO timestamp when limit resets
limitTypestringWhich limit was hit: per_minute or daily

Retry-After Headerโ€‹

The Retry-After header indicates the minimum seconds to wait:

const response = await fetch(url);

if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
await sleep(retryAfter * 1000);
return fetch(url); // Retry
}
tip

Always respect the Retry-After header. Making requests before this time will result in continued 429 responses and may trigger stricter limits.

Implementing Rate Limit Handlingโ€‹

Basic Retry Logicโ€‹

async function fetchWithRateLimit(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);

if (response.status === 429) {
if (attempt === maxRetries) {
throw new Error('Rate limit exceeded after max retries');
}

const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
console.log(`Rate limited. Waiting ${retryAfter}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}

return response;
}
}

Proactive Rate Limitingโ€‹

class RateLimitedClient {
constructor() {
this.remaining = null;
this.resetAt = null;
}

async fetch(url, options = {}) {
// Check if we know we're out of quota
if (this.remaining === 0 && this.resetAt) {
const waitMs = this.resetAt - Date.now();
if (waitMs > 0) {
console.log(`Waiting ${Math.ceil(waitMs/1000)}s for rate limit reset...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
}

const response = await fetch(url, options);

// Update rate limit tracking from headers
this.remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const resetTimestamp = parseInt(response.headers.get('X-RateLimit-Reset'));
this.resetAt = resetTimestamp ? resetTimestamp * 1000 : null;

return response;
}
}

Tipsโ€‹

  1. Authenticate โ€” SIWE doubles daily limits
  2. Use batch endpoints โ€” One call for multiple items
  3. Cache responses โ€” Price data valid for 30s
  4. Use API Keys โ€” For high-volume needs
  5. Monitor headers โ€” Track remaining requests proactively

Free Endpointsโ€‹

These endpoints never count against rate limits:

EndpointPurpose
/api/healthServer health check
/api/usageCheck your rate limit status
/api/sources/healthExternal source status
/api/cache/statsCache statistics
/docsSwagger documentation
/.well-known/x402x402 payment discovery
/api/free/*All free-tier endpoints
# These never count against limits
curl https://api.web3identity.com/api/health
curl https://api.web3identity.com/api/usage

Rate Limit Tiers Summaryโ€‹

TierDailyPer MinuteTrackingPayment After Limit
Anonymous10030By IPx402 micropayment
SIWE20030By walletx402 micropayment
API Key50060By keyPrepaid credits