> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loci.ng/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits and caps

> What is metered, what is capped, what a question costs, and what each one does when you reach it.

Three different things bound an answer, and they behave differently. **Rate
limits** refuse a request with `429` and a `Retry-After`. **Caps** never
refuse: they return less and say so. **Allowances** meter questions and
per-query datasets against the organisation's plan and credits; past them,
the answer still comes back with less in it.

## Rate limits

Every call is metered by a token bucket. A bucket holds its burst and refills
continuously at the sustained rate.

| Bucket | Sustained | Burst | Counted per |
| - | - | - | - |
| API key | 60 / min | 120 | key |
| Organisation | 600 / min | 900 | organisation |
| `POST /v1/frames/insight` | 20 / min | 30 | key, on top of the two above |
| Invalid keys | 5 / min | 5 | the presented secret |

A key spends from two buckets at once: its own and its organisation's.
Minting ten keys does not multiply the budget; it divides the same 600 a
minute between them. The question call stacks a third, tighter bucket on top,
because each question is a model call.

Every metered response carries your position, so a client can slow down
before it is refused:

| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | The bucket's capacity |
| `X-RateLimit-Remaining` | Requests left |
| `X-RateLimit-Reset` | Seconds until the bucket is full again |
| `Retry-After` | On a `429` only: seconds until the next request will pass |

When two buckets apply, the headers report whichever is closer to exhaustion.
If the headers are absent, nothing was metered: the limiter fails open when
its store is unreachable, and says nothing rather than report a budget it did
not enforce.

Honour `Retry-After`. It is computed from how long the bucket needs, so
retrying sooner cannot succeed.

<CodeGroup>
  ```python Python theme={null}
  import time, requests

  def call(url, key, attempts=5):
      for _ in range(attempts):
          r = requests.get(url, headers={"Authorization": f"Bearer {key}"})
          if r.status_code != 429:
              return r
          time.sleep(int(r.headers.get("Retry-After", 5)))
      raise RuntimeError("still rate limited after retrying")
  ```

  ```javascript JavaScript theme={null}
  async function call(url, key, attempts = 5) {
    for (let i = 0; i < attempts; i++) {
      const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
      if (res.status !== 429) return res;
      const wait = Number(res.headers.get("Retry-After") ?? 5);
      await new Promise((r) => setTimeout(r, wait * 1000));
    }
    throw new Error("still rate limited after retrying");
  }
  ```
</CodeGroup>

## Caps

A cap never refuses. It returns the most the call will produce in one
response, and where that could be mistaken for a complete answer it says so.

| Call | Cap | How you know |
| - | - | - |
| `GET /v1/frames` with `bbox` | 5,000 frames, or `limit` | `"truncated": true` |
| `GET /v1/frames` with `ids` | 100 ids | `400 invalid` |
| `GET /v1/frames/data` | 100 ids; 200 features per dataset per request | `400 invalid`; features beyond 200 are not returned |
| `POST /v1/frames/insight` | 50 frames; 1,000-character question | `400 invalid`; `400 too_long` |

There is no paging. Narrow the box, or ask about fewer frames.

## What a question costs

`POST /v1/frames/insight` has a model word its figures, and each worded
answer counts against the organisation's monthly AI allowance: 100 answers a
month on Trial, 2,000 on Pro, 20,000 on Scale. Past the allowance, answers
draw on the organisation's credits. When neither covers a question, the
figures still come back and `notice` says why they are unworded; `engine`
reads `deterministic` on such an answer. Any Admin can see the month's usage
and top up under **Billing** in the app.

A daily ceiling of 1,000 worded answers per organisation applies on top, as a
backstop against a runaway loop.

## Per-query datasets

A dataset licensed per query, rather than free or by subscription, is charged
once per request in which it contributed something, on both
`GET /v1/frames/data` and `POST /v1/frames/insight`. The ids charged are
listed in `charged`. A dataset the credits cannot cover is left out and named
in `leftOut` with the reason, and the rest of the answer arrives as usual. A
dataset that records nothing on the frames asked about is never charged.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.