# Rate limits

> How many requests BatteryPass accepts, and how to back off after a 429.

BatteryPass limits requests to protect the platform and the other organisations on it. A request over a limit
is answered with `429 rate_limited` and a `Retry-After` header.

## Limits

| Limit        | Counted for                              | Window     | Applies to                                                                              |
| ------------ | ---------------------------------------- | ---------- | --------------------------------------------------------------------------------------- |
| 600 requests | All API keys of an organisation together | 1 minute   | Every request with a valid key, whatever its result                                     |
| 30 attempts  | One client address                       | 15 minutes | Requests with an unknown or revoked key                                                 |
| 300 requests | One client address                       | 1 minute   | Public passport reads, public documents and the GS1 resolver (`/01/{gtin}/21/{serial}`) |

The windows are fixed: a window starts, counts up and is reset when it ends. All keys of an organisation share
the 600 requests; more keys do not raise the limit.

## The 429 response

```http title="429"
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 17
X-Request-Id: 8b1e6f03c2d94a7e5f0a1b2c

{
  "type": "about:blank",
  "title": "Too Many Requests",
  "status": 429,
  "code": "rate_limited",
  "detail": "Too many attempts. Try again later.",
  "request_id": "8b1e6f03c2d94a7e5f0a1b2c"
}
```

`Retry-After` is the number of seconds until the current window ends. A request answered with `429` was not
carried out, so you can repeat it after the wait, writes included.

## Back off

Wait at least `Retry-After` seconds, add a little random jitter so that parallel workers do not all start at
the same moment, and give up after a few attempts:

```ts title="TypeScript"
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Repeats a request after 429 responses, waiting as Retry-After says.
async function fetchWithBackoff(url: string, init: RequestInit = {}, attempts = 5): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(url, init);
    if (res.status !== 429 || attempt === attempts) return res;
    const seconds = Number(res.headers.get('Retry-After')) || 2 ** attempt;
    await sleep(seconds * 1000 + Math.random() * 1000);
  }
}

const res = await fetchWithBackoff(`${process.env.BATTERYPASS_URL}/api/v1/battery-models`, {
  headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` },
});
console.log(res.status);
```

```python title="Python"
import os
import random
import time

import requests


def get_with_backoff(url, attempts=5, **kwargs):
    """Repeats a GET after 429 responses, waiting as Retry-After says."""
    for attempt in range(1, attempts + 1):
        res = requests.get(url, timeout=30, **kwargs)
        if res.status_code != 429 or attempt == attempts:
            return res
        delay = int(res.headers.get("Retry-After", 2 ** attempt))
        time.sleep(delay + random.random())


res = get_with_backoff(
    f"{os.environ['BATTERYPASS_URL']}/api/v1/battery-models",
    headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"},
)
print(res.status_code)
```

```go title="Go"
package main

import (
	"fmt"
	"log"
	"math/rand/v2"
	"net/http"
	"os"
	"strconv"
	"time"
)

// doWithBackoff repeats a request without a body after 429 responses, waiting as Retry-After says.
func doWithBackoff(client *http.Client, req *http.Request, attempts int) (*http.Response, error) {
	for attempt := 1; ; attempt++ {
		res, err := client.Do(req)
		if err != nil || res.StatusCode != http.StatusTooManyRequests || attempt == attempts {
			return res, err
		}
		res.Body.Close()
		wait := time.Duration(1<<attempt) * time.Second
		if s, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil {
			wait = time.Duration(s) * time.Second
		}
		time.Sleep(wait + time.Duration(rand.IntN(1000))*time.Millisecond)
	}
}

func main() {
	req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/battery-models", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY"))
	res, err := doWithBackoff(&http.Client{Timeout: 30 * time.Second}, req, 5)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

## Stay below the limits

- **Share the budget.** All systems of your organisation draw from the same 600 requests per minute. Give
  each job a share, for example by throttling a nightly sync to a few requests per second.
- **Read in pages of 200.** One page of [`listBatteries`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-batteries)
  replaces 200 single reads; see [Pagination](https://passport.kabin.sh/docs/en/pagination).
- **Use exports for bulk data.** [`exportUnits`](https://passport.kabin.sh/docs/en/api-reference/catalog/export-units) returns up to
  10 000 units with their unit-level values as one spreadsheet (XLSX or CSV).
- **Do not retry `401`.** Fix the key instead; failed attempts count towards the 30 per 15 minutes.
- **Cache public reads.** Public passports are cached for five minutes; reading the same passport more often
  gains nothing.

If your integration needs more than these limits allow, contact [support](https://passport.kabin.sh/docs/en/general/support).
