# Ratenlimits

> Wie viele Anfragen BatteryPass annimmt und wie Sie nach einem 429 zurückhaltend wiederholen.

BatteryPass begrenzt Anfragen, um die Plattform und die anderen Organisationen darauf zu schützen. Eine
Anfrage über einem Limit wird mit `429 rate_limited` und einem Header `Retry-After` beantwortet.

## Limits

| Limit        | Gezählt für                                    | Zeitfenster | Gilt für                                                                                             |
| ------------ | ---------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
| 600 Anfragen | Alle API-Schlüssel einer Organisation zusammen | 1 Minute    | Jede Anfrage mit gültigem Schlüssel, unabhängig vom Ergebnis                                         |
| 30 Versuche  | Eine Client-Adresse                            | 15 Minuten  | Anfragen mit unbekanntem oder widerrufenem Schlüssel                                                 |
| 300 Anfragen | Eine Client-Adresse                            | 1 Minute    | Öffentliche Abrufe von Pässen, öffentliche Dokumente und der GS1-Resolver (`/01/{gtin}/21/{serial}`) |

Die Zeitfenster sind fest: Ein Fenster beginnt, zählt hoch und wird an seinem Ende zurückgesetzt. Alle
Schlüssel einer Organisation teilen sich die 600 Anfragen; weitere Schlüssel erhöhen das Limit nicht.

## Die Antwort 429

```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` ist die Zahl der Sekunden bis zum Ende des aktuellen Zeitfensters. Eine mit `429` beantwortete
Anfrage wurde nicht ausgeführt; Sie können sie nach der Wartezeit wiederholen, auch Schreibzugriffe.

## Zurückhaltend wiederholen

Warten Sie mindestens `Retry-After` Sekunden, fügen Sie etwas zufällige Streuung hinzu, damit parallele Worker
nicht alle im selben Moment starten, und geben Sie nach einigen Versuchen auf:

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

// Wiederholt eine Anfrage nach 429-Antworten und wartet so lange, wie Retry-After sagt.
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):
    """Wiederholt ein GET nach 429-Antworten und wartet so lange, wie Retry-After sagt."""
    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 wiederholt eine Anfrage ohne Body nach 429-Antworten und wartet so lange, wie Retry-After sagt.
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)
}
```

## Unter den Limits bleiben

- **Das Budget teilen.** Alle Systeme Ihrer Organisation schöpfen aus denselben 600 Anfragen pro Minute. Geben
  Sie jedem Job einen Anteil, etwa indem Sie eine nächtliche Synchronisation auf wenige Anfragen pro Sekunde
  drosseln.
- **In Seiten zu 200 lesen.** Eine Seite von
  [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) ersetzt 200 Einzelabrufe; siehe
  [Paginierung](https://passport.kabin.sh/docs/de/pagination).
- **Für Massendaten Exporte nutzen.** [`exportUnits`](https://passport.kabin.sh/docs/de/api-reference/catalog/export-units) liefert bis
  zu 10 000 Einheiten mit ihren Werten auf Einheitenebene als eine Tabelle (XLSX oder CSV).
- **`401` nicht wiederholen.** Korrigieren Sie stattdessen den Schlüssel; fehlgeschlagene Versuche zählen auf
  die 30 pro 15 Minuten.
- **Öffentliche Abrufe zwischenspeichern.** Öffentliche Pässe werden fünf Minuten zwischengespeichert;
  häufigeres Lesen desselben Passes bringt nichts.

Wenn Ihre Integration mehr braucht, als diese Limits erlauben, wenden Sie sich an den
[Support](https://passport.kabin.sh/docs/de/general/support).
