# Rate limits
URL: https://docs.fluenta.io/en/docs/rate-limits

How many requests a minute the API takes per credential and per plan, the X-RateLimit headers, the 429 and how to retry correctly.



The API counts each credential's requests in windows of 60 seconds. Past a
budget it answers 429 `RATE_LIMITED` and tells you how long to wait.

## Budgets [#budgets]

A request is counted against every budget it falls under.

| Budget           | What it counts                                                            | Light | Pro |
| ---------------- | ------------------------------------------------------------------------- | ----- | --- |
| Per organization | One credential's requests in one organization, on any route               | 60    | 120 |
| Heavy routes     | That same pair, on the heavy routes                                       | 20    | 20  |
| Credential-wide  | One credential's requests across all its organizations, added up          | 300   | 300 |
| Account routes   | `getMe`, `getMyUsage` and `listOrganizations`, which name no organization | 60    | 60  |

* The numbers are requests per 60-second window.
* Light or Pro is the plan of the organization in the path. A trial of Pro and
  an exempt organization count as Pro.
* A heavy request spends one of the heavy budget and one of the
  per-organization budget.
* The account routes are counted apart from the other three budgets.
* The limit is per credential: each API token has its own budgets.

`getPlan` answers an organization's limits in `rateLimits`: `windowSeconds`,
`organization`, `heavy`, `credential` and `account`.

## Heavy routes [#heavy-routes]

They are the ones marked `x-rate-limit-heavy: true` in the OpenAPI document:

| Operation                                      | What it reads                                                                                  |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `getFloor`                                     | The whole floor in one answer                                                                  |
| `getEfficiency`, `getProduction`, `getBilling` | A figure over a range of days                                                                  |
| `getOperatorReport`                            | The report of one «operario» (operator) over a range                                           |
| `getModuleEfficiency`, `getOperatorEfficiency` | The «Eficiencia» (efficiency) of one «módulo» (production line) or one «operario» over a range |
| `getMonth`                                     | The month                                                                                      |
| `getPauses`                                    | The «Pausas» (pauses) report                                                                   |

Except for `getFloor`, they are report routes: they answer by the
organization's plan. See [Plans](/en/docs/plans).

## The window [#the-window]

The window is fixed. It opens with a budget's first request and ends 60
seconds later, whatever came in between. When it ends, the count starts over.

## Headers [#headers]

Every answer carries them, an error included:

| Header                  | Value                                                         |
| ----------------------- | ------------------------------------------------------------- |
| `X-RateLimit-Limit`     | Requests per window of the budget you are closest to spending |
| `X-RateLimit-Remaining` | Requests left in that budget's window. 0 on a 429             |
| `X-RateLimit-Reset`     | When that window ends, in epoch seconds (UTC)                 |
| `Retry-After`           | On a 429 only: seconds you must wait                          |

`getMe` answers the same in `rateLimit` (`limit`, `remaining`, `reset`) for the
budget of the account routes.

## The 429 [#the-429]

```text
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1794499260
Retry-After: 23
```

```json
{
  "statusCode": 429,
  "code": "RATE_LIMITED",
  "retryAfterSeconds": 23,
  "message": "Demasiadas solicitudes. Espera 23 segundos antes de volver a intentar."
}
```

`retryAfterSeconds` repeats the value of `Retry-After`. The same call before
then is refused again.

## What counts and what does not [#what-counts-and-what-does-not]

* **Errors count too.** A request with a valid credential spends its budget
  even when the answer is a 4xx.
* A 403 `INSUFFICIENT_SCOPE` and a 404 `ORGANIZATION_NOT_FOUND` are counted
  against the budget of the account routes, whatever the route.
* **A 429 counts against nothing.** Asking again does not push your window
  out.
* A 401 `INVALID_API_TOKEN` does not count either: there is no verified
  credential to charge it to.

## Retrying correctly [#retrying-correctly]

Wait what `Retry-After` says and cap the attempts. Read the token from an
environment variable, not from the code.

```js
const BASE = 'https://api.fluenta.io/v1/external';

async function fluenta(path, maxRetries = 5) {
  for (let attempt = 0; ; attempt += 1) {
    const response = await fetch(`${BASE}${path}`, {
      headers: { Authorization: `Bearer ${process.env.FLUENTA_TOKEN}` },
    });

    if (response.status !== 429) return response;
    if (attempt >= maxRetries) throw new Error('Rate limited: retries spent');

    const seconds = Number(response.headers.get('Retry-After')) || 1;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}

const response = await fluenta('/organizations/42/floor');
```

```python
import os
import time

import requests

BASE = "https://api.fluenta.io/v1/external"


def fluenta(path, max_retries=5):
    headers = {"Authorization": f"Bearer {os.environ['FLUENTA_TOKEN']}"}

    for attempt in range(max_retries + 1):
        response = requests.get(f"{BASE}{path}", headers=headers, timeout=30)

        if response.status_code != 429:
            return response
        if attempt < max_retries:
            time.sleep(int(response.headers.get("Retry-After", "1")))

    raise RuntimeError("Rate limited: retries spent")


response = fluenta("/organizations/42/floor")
```

To stay clear of the 429, watch `X-RateLimit-Remaining`: when it reaches 0,
wait until `X-RateLimit-Reset` before the next request.

## Seeing your own usage [#seeing-your-own-usage]

`getMyUsage` answers what your credential asked of the API:

```bash
curl 'https://api.fluenta.io/v1/external/me/usage?days=7' \
  -H 'Authorization: Bearer fluenta_live_xxxxxxxxxxxxxxxx'
```

* `days` goes from 1 to 90 and is 30 when you leave it out. Usage is kept 90
  days.
* `totals` adds up the period, `endpoints` splits it by method and route, and
  `days` by day.
* Each carries `requests` (refusals included), `errors` (the answers 400 or
  above), `averageDurationMs`, `maxDurationMs` and `lastAt`.
* Days are UTC days, not the plant's.
* It counts this credential only, in all its organizations. The figures may
  trail by a few seconds.
