Rate limits
The two quotas every request is checked against, the headers that report them, and how to back off correctly after a 429.
Requests are metered per token against two quotas that run in parallel. The
burst quota gives you headroom for short spikes; the sustained quota is
your hourly allowance. Exceed either and the API returns
429 TOO_MANY_REQUESTS until that window replenishes.
Quotas
| Policy | Limit | Window | Keyed on |
|---|---|---|---|
| burst | 60 requests | 1 minute | Token |
| sustained | 5,000 requests | 1 hour | Token |
Both are evaluated on every request, and both must have room for the request to proceed. A steady 60 requests per minute would exhaust the sustained quota in under an hour and a half — pace against the sustained figure, not the burst one.
Search
Search is metered on top of those, with its own far tighter pair — it's the most expensive call we serve, and the only one that reads Savee's public library rather than your own data.
| Policy | Limit | Window |
|---|---|---|
| search-burst | 20 searches | 5 minutes |
| search-weekly | 200 searches | 7 days |
A search costs one general request and one search request, so all four
policies appear in the RateLimit header. Pace against the weekly figure: 200
searches a week is a deliberate ceiling, not an oversight, and it's what keeps
the library from being enumerated a query at a time. A search refused by the
5-minute window doesn't spend weekly quota.
If your integration needs more, talk to us rather than working around it — email hey@savee.com.
Requests that arrive without a usable token — missing, malformed, or unknown — are metered by IP instead, at 30 requests per minute. That bucket exists to blunt token guessing; it isn't a quota you should ever be designing against.
RateLimit headers
Every response carries two headers describing your quota state — including error
responses, and including requests that never got as far as authenticating.
Read them and pace your traffic accordingly — don't guess when a window
replenishes, and don't retry in a tight loop after a 429.
Each header is a list with one entry per active policy. The quoted string is the
policy name; the remaining tokens are parameters. Both burst and sustained
always appear on token-authenticated requests, so you never have to infer one
window's state from the other.
RateLimit-Policy
Declares the quota and window of each policy. These values are fixed and only change if your limits are raised.
| Variable | Type | Description |
|---|---|---|
| name | string | Policy identifier — burst, sustained, or unauthenticated. |
| q | integer | Total requests granted for the window. |
| w | integer | Window length, in seconds. |
RateLimit
Reports each policy's state after the current request has been charged.
| Variable | Type | Description |
|---|---|---|
| name | string | Policy identifier matching an entry in RateLimit-Policy. |
| r | integer | Requests remaining in the current window. 0 means the policy is exhausted. |
| t | integer | Seconds until the window resets. After a 429, the exhausted policy's t serves the same purpose as Retry-After. |
Legacy X-RateLimit-* headers
The older X-RateLimit-Bucket, -Limit, -Remaining and -Reset headers
are still sent and aren't going anywhere. They can only describe one quota, so
they describe whichever is closest to exhaustion — the one you'd need to pace
against. Prefer the RateLimit headers: they show you every policy at once.
Backing off after a 429
A rate-limited request returns 429 with Retry-After, in seconds:
Sleep for Retry-After seconds, then retry. If you'd rather compute it
yourself, parse the RateLimit header, find every policy with r=0, and sleep
until the slowest of those windows resets — the largest t among the exhausted
policies. Waiting against a policy that still has requests left will retry too
soon and trip the limit again.
A rejected request is only charged to the policy that rejected it. In the
example above the burst window is spent while sustained still reports 4,940 —
being rate-limited doesn't burn your hourly budget.
If you're retrying repeatedly, add jitter. A fleet of clients that all wake at the same instant re-exhausts the window together.
Staying under the limit
- Ask for bigger pages instead of more requests.
limit=100costs one request; four pages of 25 cost four. The sustained quota counts requests, not saves. - Cache on your side. Responses carry
Cache-Control: private, no-cacheso shared caches don't hold your data, but nothing stops you keeping your own. - Poll on a sensible interval. Saves don't arrive every second, and a once-a-minute poll costs 60 requests an hour against a 5,000 budget — a once-a-second poll cannot fit at all.
- Back off on failure, not just on 429. Retrying a
500in a tight loop spends the same quota as retrying a429.
MCP
The MCP server is metered the same way — the same two quotas and the
same RateLimit headers, keyed on the OAuth access token instead of an API
token. Exceeding either returns 429 with Retry-After and a JSON-RPC error
carrying retryAfterSeconds. Most MCP clients handle that for you.