Rate Limits
Savanto implements rate limiting to ensure fair usage and platform stability. Limits vary by subscription tier.
Per-Minute and Per-Hour Limits
| Tier | Requests/Minute | Requests/Hour |
|---|---|---|
| Personal | 100 | 1,000 |
| Starter | 150 | 1,500 |
| Professional | 200 | 2,000 |
| Enterprise | 300 | 5,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):
| Feature | Personal | Starter | Professional | Enterprise |
|---|---|---|---|---|
| Search Queries | 3,000 | 10,000 | 50,000 | 150,000+ |
| Chat Messages | 250 | 1,200 | 4,000 | 12,000+ |
| Recommendations | 500 | 1,800 | 5,000 | 25,000+ |
| Products | 500 | 1,500 | 5,000 | 50,000+ |
| Content Pages | 250 | 500 | 2,500 | 25,000+ |
| Crawl Pages | 250 | 500 | 2,500 | Custom |
| Webhook Deliveries | 1,000 | 5,000 | 20,000 | 100,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.