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
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
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.
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
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
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1794499260
Retry-After: 23{
"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
- Errors count too. A request with a valid credential spends its budget even when the answer is a 4xx.
- A 403
INSUFFICIENT_SCOPEand a 404ORGANIZATION_NOT_FOUNDare 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_TOKENdoes not count either: there is no verified credential to charge it to.
Retrying correctly
Wait what Retry-After says and cap the attempts. Read the token from an
environment variable, not from the code.
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');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
getMyUsage answers what your credential asked of the API:
curl 'https://api.fluenta.io/v1/external/me/usage?days=7' \
-H 'Authorization: Bearer fluenta_live_xxxxxxxxxxxxxxxx'daysgoes from 1 to 90 and is 30 when you leave it out. Usage is kept 90 days.totalsadds up the period,endpointssplits it by method and route, anddaysby day.- Each carries
requests(refusals included),errors(the answers 400 or above),averageDurationMs,maxDurationMsandlastAt. - 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.