# Rate limits

1800 requests per 30 minutes and 5 bad requests per 60 seconds, per company.

## The limits

| What | Limit | Window |
| --- | --- | --- |
| Requests | 1800 | 30 minutes |
| Bad requests | 5 | 60 seconds |

A "bad request" is any response with status code `400` through `500`. Bad requests count in both meters: as a normal request and as a bad request. So fix auth and parameter shape before you start a loop.

## What it counts per

The budget belongs to your **company**, not to an IP address and not to a single machine. Calling from three servers does not give you three budgets: they draw on the same one.

The numbers above are the standard budget. They are not a hard ceiling on what the platform can give you: we raise them per customer when an integration genuinely needs more, and several customers already run higher. If you are planning a bulk export or a nightly sync that will not fit, [talk to us](https://geodynamics.eu/nl-be/support/) before you build around the limit.

## Throttle response

Above the limit you get `HTTP 429 Too Many Requests` back, with a `Retry-After` header telling you how many seconds you're still blocked.

```http title="throttle.txt"
HTTP/1.1 429 Too Many Requests
Retry-After: 180
Content-Type: application/json

{ "error": "rate_limited" }
```

## How the throttle window grows

Once you hit the request limit, you're blocked for 3 minutes for each extra request you still try. That window grows until 30 minutes have passed since you hit the limit. In practice: stop trying, wait the `Retry-After` value, then resume at a calmer pace.

| Status | Description |
| --- | --- |
| `429` | Too Many Requests |
| `401` | Unauthorized (counts as a bad request) |

## Respect Retry-After

```node title="respect-retry-after.ts"
async function call(url: string, headers: HeadersInit): Promise<Response> {
  const res = await fetch(url, { headers });

  if (res.status === 429) {
    const seconds = parseInt(res.headers.get("retry-after") ?? "60", 10);
    await new Promise(r => setTimeout(r, seconds * 1000));
    return call(url, headers);
  }

  return res;
}
```

```python title="respect-retry-after.py"
import time, requests

def call(url, headers):
    res = requests.get(url, headers=headers, timeout=10)
    if res.status_code == 429:
        seconds = int(res.headers.get("Retry-After", "60"))
        time.sleep(seconds)
        return call(url, headers)
    return res
```

## Stay under the limit

-   Cache what you already have. Resource groups, workplaces, and day programs don't change often. Refresh on a schedule, not per user action.
-   Ask broadly instead of narrowly. One call with a date range is cheaper than six calls per individual day.
-   Avoid hot loops. A last-position poll every 5 seconds eats your budget in 2.5 hours. Vehicles update every 1 to 3 minutes typically, polling faster gains you nothing.
-   Fix bad requests first. Five consecutive `401`s lock you out for a minute. Test credentials in dev before you ship to prod.
-   Call server-side, not from the browser. One central proxy with caching shares the work, and your secret stays out of client bundles.

## More

-   [Errors](/docs/errors) for the status codes and what each one means.
-   [Authentication](/docs/authentication) to avoid `401` storms.
