Rate Limits
To keep the platform fast and fair for every tenant, the Weave API limits how many requests each environment can send.
How limits work
Each environment has its own allowance, shared by every credential that calls on its behalf: users, M2M clients and API keys. A single API key may use the whole allowance.
Limits are token buckets. Each one refills at a sustained rate per second, and its burst is the most requests it can hold for a short spike. An idle bucket fills back up to its burst.
Every endpoint belongs to one of six buckets. GET, HEAD and OPTIONS requests count as reads, and every other method counts as a write.
| Bucket | What it covers | Sustained | Burst |
|---|---|---|---|
| Operational read | Day-to-day reads: customers, program accounts, ledgers, coupons, badges, search | 100/s | 200 |
| Operational write | Day-to-day writes on the same resources | 30/s | 60 |
| Events write | POST /events | 100/s | 300 |
| Events read | Event lists and traces | 30/s | 60 |
| Management read | Configuration reads: programs, tiers, offers, data models, rules, templates, settings | 50/s | 100 |
| Management write | Configuration writes on the same resources | 5/s | 20 |
These are the production limits on a Base plan. Weave MCP tool calls count against the same buckets as the equivalent API calls, with read tools counting as reads and write tools counting as writes.
A few endpoints also keep a tighter limit of their own. For example, search has a per-user limit within operational read.
Limits for your plan
Your plan scales every number in the table:
| Plan | Multiplier |
|---|---|
| Free, Test | 0.2× |
| Base | 1× |
| Enterprise L1 | 2× |
| Enterprise L2 | 3× |
| Enterprise L3 | 5× |
| Enterprise L4 | 10× |
| Enterprise L5 | 20× |
Development and staging environments
Development and staging environments get 25% of your plan's production limits. Management writes are the exception: they keep the production limit, so configuration imports and promotions run at full speed.
Whatever the plan and environment, no limit drops below 5 requests per second with a burst of 20.
For example, on Enterprise L2 a staging environment gets 75 operational reads per second (100 × 3 × 25%) and 15 management writes per second (5 × 3, with no discount).
If you need more than your plan allows, contact support@loomloyalty.com. Limits for an individual organization can be raised without a plan change.
429 Too Many Requests
When a bucket is empty, the API responds with 429 Too Many Requests and these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before retrying. Always at least 1. |
X-RateLimit-Limit | The bucket's capacity, which is its burst. |
X-RateLimit-Remaining | Requests left in the bucket. |
X-RateLimit-Reset | Seconds until the bucket is full again. |
The body uses the standard error envelope. errors[0].description is RATE_LIMITED:
{
"success": false,
"errors": [
{
"code": 429,
"message": "Rate limit exceeded. Retry after the number of seconds in the Retry-After header.",
"description": "RATE_LIMITED"
}
]
}
503 Service Unavailable
When the platform is briefly too busy to start your request, the API waits about a second and then responds with 503 Service Unavailable and a Retry-After header. errors[0].description is SERVICE_OVERLOADED. This response doesn't use up your rate limit, so retry it the same way as a 429.
Backing off
Retry 429 and 503 responses after the Retry-After delay. When the header is missing, back off exponentially. In both cases, add random jitter so that many clients don't all retry at the same moment.
async function requestWithBackoff(url: string, options: RequestInit, maxRetries = 5) {
for (let attempt = 0; ; attempt++) {
const response = await fetch(url, options);
const retryable = response.status === 429 || response.status === 503;
if (!retryable || attempt === maxRetries) return response;
const retryAfter = Number(response.headers.get("Retry-After"));
const baseSeconds =
Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : Math.min(2 ** attempt, 30);
const jitterSeconds = Math.random() * baseSeconds * 0.5;
await new Promise((resolve) => setTimeout(resolve, (baseSeconds + jitterSeconds) * 1000));
}
}
Best practices
- Back off and retry on
429and503instead of immediately hammering the endpoint again. - Watch
X-RateLimit-Remainingon a429, and slow a bulk job down before it runs dry again. - Cache responses that change infrequently rather than re-fetching them.
- Page through data with the
pageandsizeparameters (see Pagination & Filtering) instead of requesting everything at once. - Spread out large bulk operations rather than firing them all at the same instant.