developers/Rate Limits

Rate Limits

Savanto implements rate limiting to ensure fair usage and platform stability. Limits vary by subscription tier.

Per-Minute and Per-Hour Limits

TierRequests/MinuteRequests/Hour
Personal1001,000
Starter1501,500
Professional2002,000
Enterprise3005,000

Monthly Quotas

Feature-specific monthly quotas (Enterprise quotas are contractual — the figures below are the current default ceiling for the provisioned tier; contact sales for higher limits or custom shapes):

FeaturePersonalStarterProfessionalEnterprise
Search Queries3,00010,00050,000150,000+
Chat Messages2501,2004,00012,000+
Recommendations5001,8005,00025,000+
Products5001,5005,00050,000+
Content Pages2505002,50025,000+
Crawl Pages2505002,500Custom
Webhook Deliveries1,0005,00020,000100,000+

For current pricing on the self-serve plans see savanto.ai/pricing. Enterprise pricing is sales-led — see Talk to Sales.

Monitoring Usage

Every API response includes rate limit and quota data in the response body:

{
  "usage": {
    "rateLimit": {
      "remaining": { "perMinute": 45, "perHour": 950 },
      "resetTime": 1733180400000
    },
    "quota": {
      "current": 150,
      "limit": 10000,
      "resetDate": "2026-04-01"
    }
  }
}
import { createClient, searchProducts } from '@savantoai/ai-sdk';

const client = createClient({
  baseUrl: 'https://api.savanto.ai',
  auth: process.env.SAVANTO_SECRET_KEY!,
});

const response = await searchProducts({
  client,
  body: { text: 'running shoes' },
});

if (response.data?.usage?.quota) {
  const { current, limit } = response.data.usage.quota;
  console.log(`Quota: ${current} / ${limit}`);
}

Handling 429 Responses

When you exceed rate limits, the API returns 429 Too Many Requests:

{
  "error": {
    "message": "Rate limit exceeded",
    "code": "RATE_LIMITED"
  },
  "retryAfter": 30
}

Exponential Backoff

The SDK resolves with { data, error, response } rather than throwing, so a retry wrapper inspects the result instead of catching:

// Only a burst limit is worth waiting out. A spent monthly allowance
// (`QUOTA_EXCEEDED`) resets on the 1st, and a full document capacity
// (`INDEXING_LIMIT_EXCEEDED`) clears only when you remove content or upgrade —
// backing off on either just burns attempts before failing anyway.
const PERMANENT = ['QUOTA_EXCEEDED', 'INDEXING_LIMIT_EXCEEDED'];

async function withRetry<T extends { error?: unknown; response: Response }>(
  call: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  let result = await call();

  for (let attempt = 0; attempt < maxRetries - 1; attempt++) {
    if (!result.error || result.response.status !== 429) return result;

    const code = (result.error as { error?: { code?: string } }).error?.code;
    if (code && PERMANENT.includes(code)) return result;

    await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
    result = await call();
  }

  return result;
}

const { data, error } = await withRetry(() =>
  searchProducts({ client, body: { text: 'query' } })
);

If you configured the client with throwOnError, the thrown value is the parsed error body itself — read err.error.code, not err.response.data.

Document Capacity on Bulk Upserts

A bulk upsert that runs into your plan's document limit does not fail outright. The API indexes what fits and accounts for the remainder, so updates to documents you have already indexed keep working even at your limit. Sending four products with one slot left:

{
  "processed": 3,
  "succeeded": ["prod-1", "prod-2", "prod-3"],
  "failed": [],
  "skipped": 1,
  "indexingLimitReached": true
}

Read succeeded rather than assuming the whole batch landed — anything absent from it was not indexed, and skipped counts the documents refused for capacity. The status is 200 when every admitted document succeeded and 207 when any entry landed in failed, so branch on the body rather than on the status alone.

A 429 with INDEXING_LIMIT_EXCEEDED is reserved for a request where nothing could be accepted — every document in it was new and there were no slots left.

Best Practices

Use Bulk Operations

Instead of individual requests, use bulk endpoints to stay within rate limits:

# One request instead of 100
curl -X POST https://api.savanto.ai/products/bulk \
  -H "Authorization: Bearer if_sk_xxx" \
  -H "Content-Type: application/json" \
  -d '{"products": [...]}'

Cache Responses

Cache search results and recommendations to reduce API calls:

const cacheKey = `search_${query}`;
let results = cache.get(cacheKey);

if (!results) {
  const response = await searchProducts({
    client,
    body: { text: query },
  });
  results = response.data;
  cache.set(cacheKey, results, 300); // 5 minutes
}

Monitor Before You Hit Limits

Use the usage data in every response to proactively throttle before hitting 429.

Quota Resets

  • Rate limits reset per minute and per hour as specified
  • Monthly quotas reset on the 1st of each month at 00:00 UTC

Increasing Limits

  • Upgrade your tier — Professional and Enterprise have higher limits
  • Contact sales — Custom limits available for Enterprise customers
  • Optimize usage — Bulk operations and caching reduce calls significantly

Contact sales@savanto.ai to discuss your needs.