> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beaconcha.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Understanding Rate Limits

> Learn how to monitor and work within API rate limits

## Overview

The beaconcha.in API uses rate limiting to ensure fair usage and maintain service quality for all users. Rate limits vary by subscription tier and are enforced across multiple time windows.

***

## Rate Limit Headers

Every API response includes headers that show your current rate limit status:

### Primary Headers

| Header                  | Description                             | Example   |
| ----------------------- | --------------------------------------- | --------- |
| `ratelimit-limit`       | Requests allowed per window             | `1`       |
| `ratelimit-remaining`   | Requests remaining in current window    | `0`       |
| `ratelimit-reset`       | Seconds until the window resets         | `1`       |
| `ratelimit-window`      | The time window type                    | `second`  |
| `ratelimit-bucket`      | The rate limit bucket for this endpoint | `default` |
| `ratelimit-validapikey` | Whether your API key is valid           | `true`    |

### Extended Headers

The API also returns detailed limits for each time window:

| Header                         | Description                    |
| ------------------------------ | ------------------------------ |
| `x-ratelimit-limit-second`     | Requests allowed per second    |
| `x-ratelimit-limit-minute`     | Requests allowed per minute    |
| `x-ratelimit-limit-hour`       | Requests allowed per hour      |
| `x-ratelimit-limit-day`        | Requests allowed per day       |
| `x-ratelimit-limit-month`      | Requests allowed per month     |
| `x-ratelimit-remaining-second` | Remaining requests this second |
| `x-ratelimit-remaining-minute` | Remaining requests this minute |
| `x-ratelimit-remaining-hour`   | Remaining requests this hour   |
| `x-ratelimit-remaining-day`    | Remaining requests today       |
| `x-ratelimit-remaining-month`  | Remaining requests this month  |

***

## Checking Your Rate Limits

Make any API request and inspect the response headers to see your current limits:

```bash theme={null}
curl -s -D - -o /dev/null --request POST \
  --url https://beaconcha.in/api/v2/ethereum/validators/rewards-aggregate \
  --header 'Authorization: Bearer <YOUR_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "chain": "mainnet",
    "validator": { "validator_identifiers": [1] },
    "range": { "evaluation_window": "24h" }
  }'
```

**Example Response Headers:**

```
ratelimit-limit: 1
ratelimit-remaining: 0
ratelimit-reset: 1
ratelimit-window: second
ratelimit-bucket: default
ratelimit-validapikey: true
x-ratelimit-limit-second: 1
x-ratelimit-limit-minute: 1000
x-ratelimit-limit-hour: 1000
x-ratelimit-limit-day: 1000
x-ratelimit-limit-month: 1000
x-ratelimit-remaining-second: 0
x-ratelimit-remaining-minute: 972
x-ratelimit-remaining-hour: 972
x-ratelimit-remaining-day: 972
x-ratelimit-remaining-month: 972
```

***

## Handling Rate Limit Errors

When the rate limit is exceeded, the API returns a `429 Too Many Requests` status code. To handle this correctly, implement retry logic with a dynamic delay: immediately pause outgoing requests and wait for the duration specified in the `ratelimit-reset` header before attempting the request again.

### Code Examples

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import requests
    import time

    API_KEY = "<YOUR_API_KEY>"
    BASE_URL = "https://beaconcha.in"

    def make_request_with_retry(endpoint: str, payload: dict, max_retries: int = 5) -> dict:
        """Make API request with automatic retry on rate limit."""
        
        for attempt in range(max_retries):
            response = requests.post(
                f"{BASE_URL}{endpoint}",
                headers={
                    "Authorization": f"Bearer {API_KEY}",
                    "Content-Type": "application/json"
                },
                json=payload
            )
            
            if response.status_code == 200:
                return response.json()
            
            if response.status_code == 429:
                # Read the reset time from header
                reset_seconds = int(response.headers.get("ratelimit-reset", 1))
                print(f"Rate limited. Waiting {reset_seconds}s before retry...")
                time.sleep(reset_seconds)
                continue
            
            # Other errors - raise exception
            error_data = response.json()
            raise Exception(f"API error {response.status_code}: {error_data.get('error', 'Unknown error')}")
        
        raise Exception(f"Max retries ({max_retries}) exceeded")

    # Usage
    data = make_request_with_retry(
        "/api/v2/ethereum/validators/rewards-aggregate",
        {
            "chain": "mainnet",
            "validator": {"validator_identifiers": [1, 2, 3]},
            "range": {"evaluation_window": "7d"}
        }
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const API_KEY = "<YOUR_API_KEY>";
    const BASE_URL = "https://beaconcha.in";

    async function makeRequestWithRetry<T>(
      endpoint: string,
      payload: object,
      maxRetries: number = 5
    ): Promise<T> {
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(`${BASE_URL}${endpoint}`, {
          method: "POST",
          headers: {
            "Authorization": `Bearer ${API_KEY}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify(payload)
        });

        if (response.ok) {
          return response.json();
        }

        if (response.status === 429) {
          // Read the reset time from header
          const resetSeconds = parseInt(response.headers.get("ratelimit-reset") || "1", 10);
          console.log(`Rate limited. Waiting ${resetSeconds}s before retry...`);
          await new Promise(resolve => setTimeout(resolve, resetSeconds * 1000));
          continue;
        }

        // Other errors - throw exception
        const errorData = await response.json();
        throw new Error(`API error ${response.status}: ${errorData.error || "Unknown error"}`);
      }

      throw new Error(`Max retries (${maxRetries}) exceeded`);
    }

    // Usage
    const data = await makeRequestWithRetry(
      "/api/v2/ethereum/validators/rewards-aggregate",
      {
        chain: "mainnet",
        validator: { validator_identifiers: [1, 2, 3] },
        range: { evaluation_window: "7d" }
      }
    );
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    const API_KEY = "<YOUR_API_KEY>";
    const BASE_URL = "https://beaconcha.in";

    async function makeRequestWithRetry(endpoint, payload, maxRetries = 5) {
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(`${BASE_URL}${endpoint}`, {
          method: "POST",
          headers: {
            "Authorization": `Bearer ${API_KEY}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify(payload)
        });

        if (response.ok) {
          return response.json();
        }

        if (response.status === 429) {
          // Read the reset time from header
          const resetSeconds = parseInt(response.headers.get("ratelimit-reset") || "1", 10);
          console.log(`Rate limited. Waiting ${resetSeconds}s before retry...`);
          await new Promise(resolve => setTimeout(resolve, resetSeconds * 1000));
          continue;
        }

        // Other errors - throw exception
        const errorData = await response.json();
        throw new Error(`API error ${response.status}: ${errorData.error || "Unknown error"}`);
      }

      throw new Error(`Max retries (${maxRetries}) exceeded`);
    }

    // Usage
    const data = await makeRequestWithRetry(
      "/api/v2/ethereum/validators/rewards-aggregate",
      {
        chain: "mainnet",
        validator: { validator_identifiers: [1, 2, 3] },
        range: { evaluation_window: "7d" }
      }
    );
    ```
  </Tab>
</Tabs>

***

## Revoked Access

A `429` response does not always mean a temporary rate limit. If your free trial has expired or your subscription has been cancelled, all limit headers will be `0`:

```
x-ratelimit-limit-second: 0
x-ratelimit-limit-minute: 0
x-ratelimit-limit-hour:   0
x-ratelimit-limit-day:    0
x-ratelimit-limit-month:  0
```

In this case, **do not retry** — waiting will not help. The `ratelimit-reset` and `retry-after` headers are present but do not reflect a real reset time. Upgrade your plan at [beaconcha.in/api/pricing](https://beaconcha.in/api/pricing) to restore access.

***

## Rate Limits by Plan

Rate limits vary by subscription tier:

| Plan       | Price                                                        | Features       | Ratelimit                                                    | Requests  |
| ---------- | ------------------------------------------------------------ | -------------- | ------------------------------------------------------------ | --------- |
| Free Trial | 0€ (30 days)                                                 | Basic          | 1/sec                                                        | 1000      |
| Hobbyist   | 59€/mo\*                                                     | Basic          | 1/sec                                                        | Unlimited |
| Business   | 99€/mo\*                                                     | Basic          | 2/sec                                                        | Unlimited |
| Scale      | 399€/mo\*                                                    | Basic & Pro 💎 | 5/sec                                                        | Unlimited |
| Enterprise | [Contact us](https://beaconcha.in/api/pricing#contact-sales) | Basic & Pro 💎 | [Contact us](https://beaconcha.in/api/pricing#contact-sales) | Unlimited |

\*Prices shown are for annual billing, excluding VAT

<Note>
  **Pro 💎 features** include premium validator selectors (`withdrawal` address, `deposit_address`) and are available on Scale and Enterprise plans.
</Note>

***

## Rate Limit Buckets

A **rate limit bucket** is an independent counter that tracks API usage separately from other buckets. Think of each bucket as its own quota system with its own limits and usage tracking.

When you make API requests, the system checks which bucket(s) your requests count against and decrements the appropriate counter(s). Once a bucket is exhausted, requests that count against that bucket will be rate limited until the bucket resets.

### Key Concepts

* **Separate buckets** mean usage in one bucket doesn't affect the limits in another bucket
* **Shared buckets** mean all requests count against the same quota, regardless of which API version you're using
* Your subscription type determines whether V1 and V2 API requests use separate or shared buckets

### Endpoint Buckets

Different endpoints are assigned to specific buckets. You can see which bucket your request counted against in the `ratelimit-bucket` response header.

| Bucket         | Endpoints                                                    |
| -------------- | ------------------------------------------------------------ |
| `default`      | All endpoints not listed below                               |
| `app`          | `/api/v1/dashboard/widget`, `/api/v2/validator-dashboards/*` |
| `machine`      | `/api/v1/client/metrics`                                     |
| `oldsubnewapi` | `/api/v2/*` (only for legacy subscription users)             |

<Note>
  The `oldsubnewapi` bucket applies when a user with a legacy subscription (Sapphire, Emerald, Diamond) accesses V2 endpoints. This keeps V2 usage separate from their V1 quota.
</Note>

## How V1 and V2 rate limits work

### API keys

There is only **one kind of API key**. All keys work for both **V1** and **V2** endpoints.

### Rate limits depend on your subscription type

#### Legacy subscriptions

If you are on a legacy plan (**Sapphire**, **Emerald**, or **Diamond**):

* **V1 endpoints:** you get your **legacy plan rate limits**.
* **V2 endpoints:** included with **1 call/s, 1000 requests/month**.
* **Separate buckets:** V1 and V2 usage are tracked in **separate rate limit buckets**. This means:
  * Requests to V1 API endpoints do **not** count towards your V2 API rate limit
  * Requests to V2 API endpoints do **not** count towards your V1 API rate limit
  * You can fully utilize both V1 and V2 limits independently
* **Limit types:** legacy plans enforce **per-second** and **per-month** limits.

#### Current subscriptions

If you are on a current plans (**Hobbyist**, **Business**, or **Scale**):

* **V1 + V2 endpoints:** you get the **same rate limits** on both versions.
* **Same bucket:** V1 and V2 usage share **one combined rate limit bucket**.
* **Limit types:** current plans enforce **per-second** limits only (no monthly limits).

### Enterprise

Enterprise customers are handled case-by-case.

<Tip>
  View current pricing at [beaconcha.in/pricing](https://beaconcha.in/pricing). Your actual limits are always available in the response headers.
</Tip>

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Monitor Headers" icon="gauge">
    Check `x-ratelimit-remaining-*` headers before making bulk requests to avoid hitting limits.
  </Card>

  <Card title="Use Backoff" icon="clock-rotate-left">
    Implement exponential backoff when receiving 429 errors to gracefully handle rate limits.
  </Card>

  <Card title="Batch with Dashboards" icon="table-columns">
    Use `dashboard_id` to query multiple validators in a single request instead of individual calls.
  </Card>
</CardGroup>

***

## Related Resources

* [Pagination Guide](/api/pagination) — Efficient data fetching
* [Dashboard as Private Sets](/use-cases/rewards-dashboard-private-sets) — Batch validator queries
* [Custom Range Rewards](/use-cases/rewards-custom-range) — Performance considerations for bulk operations
