# Rate Limits & 429 Handling Playbook

Learn how to handle rate limits gracefully — exponential backoff, Retry-After headers, and best practices for staying within the eToro API's request budgets.

---


## Overview

Every API has rate limits — the eToro API is no exception. This guide covers how to detect, handle, and avoid rate-limit errors so your application stays resilient under load.

## Current rate limits

Limits are tracked per user key over a **1-minute rolling window**.

| Category | Limit | Applies to |
|----------|-------|------------|
| **Read (GET)** | **60 requests/min** | Market data, portfolio info, social feeds (read), watchlists (read) |
| **Write & Execution** | **20 requests/min** | Trading execution (open, close, cancel), watchlist management (create, update, delete), social feeds (write), user trade info |

> **Tip:** Cache non-volatile data locally (instrument metadata, exchange info) to preserve your quota for real-time operations like trading. For the latest details, see the [official Rate Limits documentation](https://api-portal.etoro.com/getting-started/rate-limits).

## What happens when you hit a rate limit

When your application exceeds the allowed request rate, the API responds with HTTP `429 Too Many Requests`. The response includes headers that tell you what to do next.

### Key response headers

| Header | Meaning |
|---|---|
| `Retry-After` | Seconds to wait before retrying |
| `X-RateLimit-Limit` | Maximum requests allowed in the window |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |

> Header availability may vary by endpoint. Check the [API Reference](https://api-portal.etoro.com) for details.

## Exponential backoff with jitter

When you receive a `429`, don't retry immediately. Use exponential backoff with jitter to spread retries across time and avoid a "thundering herd" of requests when the window resets.

```python skip-test
import time
import random
import requests

def fetch_with_backoff(url, headers, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.get(url, headers=headers)

        if resp.status_code != 429:
            return resp

        retry_after = int(resp.headers.get("Retry-After", 2 ** attempt))
        jitter = random.uniform(0, retry_after * 0.5)
        wait = retry_after + jitter

        print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt + 1})")
        time.sleep(wait)

    raise Exception("Max retries exceeded")
```

```javascript skip-test
async function fetchWithBackoff(url, headers, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const resp = await fetch(url, { headers });

    if (resp.status !== 429) return resp;

    const retryAfter = parseInt(resp.headers.get("Retry-After") ?? String(2 ** attempt), 10);
    const jitter = Math.random() * retryAfter * 0.5;
    const wait = retryAfter + jitter;

    console.log(`Rate limited. Waiting ${wait.toFixed(1)}s (attempt ${attempt + 1})`);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }

  throw new Error("Max retries exceeded");
}
```

## Best practices

### 1. Respect `Retry-After`

Always check the `Retry-After` header first. It tells you exactly how long to wait — no guesswork needed.

### 2. Cache when possible

Market data that doesn't change frequently (instrument metadata, exchange lists) can be cached locally. This dramatically reduces your request count.

```python skip-test
import requests

BASE = "https://public-api.etoro.com/api/v1"

# Cache instrument types — they rarely change
instrument_types = requests.get(
    f"{BASE}/market-data/instrument-types",
    headers=headers,
).json()
```

### 3. Use WebSocket for real-time data

Instead of polling `/market-data/rates` every second, subscribe to price updates via the WebSocket stream. This uses one persistent connection instead of hundreds of HTTP requests.

```javascript skip-test
const ws = new WebSocket("wss://ws.etoro.com/ws");

ws.onopen = () => {
  ws.send(JSON.stringify({
    operation: "Authenticate",
    data: { userKey: USER_KEY, apiKey: API_KEY },
  }));

  ws.send(JSON.stringify({
    operation: "Subscribe",
    data: { topics: ["instrument:100000"], snapshot: false },
  }));
};
```

### 4. Spread requests over time

If you need to fetch data for many instruments, don't fire all requests at once. Use a simple rate limiter:

```python skip-test
import time

instrument_ids = [1001, 1002, 1003, 1004, 1005]

for iid in instrument_ids:
    resp = requests.get(
        f"{BASE}/market-data/rates",
        headers=headers,
        params={"instrumentIds": iid},
    )
    print(resp.status_code)
    time.sleep(0.2)  # 5 requests/second
```

### 5. Monitor your remaining budget

Check `X-RateLimit-Remaining` on every response. When it gets low, proactively slow down before you hit the wall.

## Quick reference

| Scenario | Recommended action |
|---|---|
| Got a `429` | Wait for `Retry-After` seconds, then retry with backoff |
| `X-RateLimit-Remaining` is low | Slow down request rate proactively |
| Need real-time prices | Use WebSocket instead of polling |
| Static data (instruments, exchanges) | Cache locally, refresh periodically |
| Batch operations | Spread requests over time with a rate limiter |

## Further reading

- [Official Rate Limits Documentation](https://api-portal.etoro.com/getting-started/rate-limits) — authoritative numbers and tier details
- [Getting Started Guide](/learn/getting-started-with-etoro-api-v2) — set up your API keys
- [Real-Time Market Data via WebSocket](/learn/real-time-market-data-websocket) — reduce polling with streams
