# Rate limiting

The API uses **two-tier rate limiting**. When you go over the normal limit, cached responses still come through, up to a second, higher limit.

## How it works

Tier 1: Normal

Standard rate limit for all requests. Can fetch fresh lyrics or serve from cache.

When exceeded

Tier 2: Cached

Higher rate limit, but only serves cached responses. Fresh fetches return 429.

When exceeded

Rate limited

All requests return 429 Too Many Requests.

Limits are per IP address. The limits can change, so do not hardcode them. Read `X-RateLimit-Limit` and `X-RateLimit-Remaining` on each response instead.

## Response headers

The API reports rate limit, cache and authentication state in these headers:

| Header | Values | When it is sent |
| --- | --- | --- |
| `X-RateLimit-Limit` | Burst size of the current tier. On `exceeded` it is the burst size of the cached tier. | Every request without a valid API key |
| `X-RateLimit-Remaining` | Requests left in the current tier, `0` when both tiers are exceeded | Every request without a valid API key |
| `X-RateLimit-Type` | `normal`, `cached`, `exceeded`, or `bypass` | Every request without a valid API key. With a valid API key, `bypass` is sent on JSON responses from the lyrics endpoints, `/revalidate` and the `/cache/*` endpoints |
| `X-RateLimit-Bypass` | `true` | Requests with a valid `X-API-Key` header |
| `Retry-After` | `1` when both tiers are exceeded, `60` when the cached tier has no cache entry for the query | Only on `429` responses |
| `X-Cache-Status` | `HIT`, `MISS`, `NEGATIVE_HIT`, or `STALE` (`STALE` only on `/getLyrics`) | Lyrics endpoints: `/getLyrics` and the provider-specific endpoints. See Caching |
| `X-Provider` | `ttml`, `kugou`, or `qq` | Provider-specific endpoints only, for example `/kugou/getLyrics` |
| `X-Auth-Mode` | `authenticated`, `invalid`, or `cache` | `/getLyrics`, `/ttml/getLyrics` and `/revalidate`, when the API requires a key for uncached queries. See Authentication |

Browsers cannot read these headers from cross-origin JavaScript. The API does not expose them through CORS on purpose, so they are meant for server-side callers and tools like curl.

## Rate limit types

### `normal`

Request processed normally. Can fetch fresh lyrics or serve from cache.

### `cached`

Normal tier exceeded. The request is served only if the lyrics are cached. Otherwise it returns 429.

### `exceeded`

Both tiers exceeded. All requests return 429 Too Many Requests.

### `bypass`

The request has a valid API key, so rate limits do not apply.

## API key bypass

Requests with a valid `X-API-Key` header bypass all rate limits. This is useful for production applications that need guaranteed access.

   

```bash
curl -H "X-API-Key: your-api-key" \
  "https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154"
```

```javascript
const res = await fetch("https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154", {
  headers: { "X-API-Key": "your-api-key" },
});
const data = await res.json();
```

```python
import requests

res = requests.get(
    "https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154",
    headers={"X-API-Key": "your-api-key"},
)
data = res.json()
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://api.betterlyrics.org/getLyrics?s=E85&a=Don%20Toliver&al=OCTANE&d=154", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Key", "your-api-key")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(body))
}
```

When bypass is active, every response includes `X-RateLimit-Bypass: true`, and has no `X-RateLimit-Limit` or `X-RateLimit-Remaining`. The lyrics endpoints, `/revalidate` and the `/cache/*` JSON responses also include `X-RateLimit-Type: bypass`:

```bash
X-RateLimit-Bypass: true
X-RateLimit-Type: bypass
```

## Handling rate limits

When you receive a 429 response:

1.  Check the `Retry-After` header for when to retry
2.  Check `X-RateLimit-Type` to see which tier was exceeded
3.  If `cached`, the lyrics are not in the cache yet, so wait before you retry
4.  Use exponential backoff for retries

## Rate limit responses

When both tiers are exhausted, any endpoint returns `429 Too Many Requests` with `Content-Type: text/plain`, `Retry-After: 1` and `X-RateLimit-Type: exceeded`:

```
Too Many Requests
```

When the cached tier serves a lyrics request that is not cached, the response is `429` with a JSON body, `Retry-After: 60` and `X-RateLimit-Type: cached`:

```json
{
  "error": "Rate limit exceeded. This request requires cached data, but no cache is available for this query.",
  "message": "Please try again later or reduce your request rate."
}
```

The provider-specific endpoints return `{"error": "Rate limit exceeded. No cached data available.", "provider": "..."}` in this case.

Source: https://docs.betterlyrics.org/docs/rate-limiting
