# Rate Limits

Terra applies rate limits on data-read endpoints (e.g `/activity`)  to protect the platform from runaway loops and to keep response times consistent for everyone.

The limits operate per `(user_id)` — meaning each user you've connected has their own budget, separate from your other users.

### The rules

There are two rules. A request must satisfy **both** to be accepted.

| Rule                          | Limit                | Window                                |
| ----------------------------- | -------------------- | ------------------------------------- |
| **Per-request window size**   | 1,825 days (5 years) | per request                           |
| **Cumulative requested days** | 6,000 days           | rolling 1-hour bucket per `(user_id)` |

The "requested days" cost of a single call is `end_date − start_date`. Calls without `start_date`/`end_date` count as 1 day. Requests at exactly 1,825 days are allowed; only `> 1,825` is rejected.

{% hint style="info" %}
**Why two rules?** Most partners do small windows (≤ 30 days) and never come close to either limit. The per-request cap stops obviously-broken calls (e.g. requesting 20 years of data in one shot). The hourly cumulative cap stops loops that hammer the same user repeatedly with multi-year windows.
{% endhint %}

### Detecting throttling

When a request is rejected, you'll receive an `HTTP 429 Too Many Requests` response with these headers:

| Header                   | Meaning                                                                                                     |
| ------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `X-Terra-RateLimit-Rule` | Which rule fired: `r1` (per-request) or `r2` (cumulative)                                                   |
| `Retry-After`            | For `r2`: seconds until the hourly bucket resets. For `r1`: not set (retrying the same request won't help). |

Response body:

```json
{
  "detail": "rate limit exceeded"
}
```

### Successful responses include your remaining budget

Whenever a request is accepted and the cumulative rule (R2) was evaluated, we include three headers so you can pace yourself without guessing:

| Header                          | Meaning                                 |
| ------------------------------- | --------------------------------------- |
| `X-Terra-RateLimit-Limit`       | Your hourly limit in days (e.g. `6000`) |
| `X-Terra-RateLimit-Remaining`   | Days left in the current hour bucket    |
| `X-Terra-RateLimit-Reset-After` | Seconds until the bucket resets         |

Example response:

```
HTTP/1.1 200 OK
Content-Type: application/json
X-Terra-RateLimit-Limit: 6000
X-Terra-RateLimit-Remaining: 5910
X-Terra-RateLimit-Reset-After: 1843
...
```

If these headers are missing, no rate-limit budget was tracked for the request (e.g. an unauthenticated path) — you don't need to act on them.

### Handling 429s

* **`X-Terra-RateLimit-Rule: r1`** — your `start_date`/`end_date` window is wider than 5 years. Split it into shorter windows and retry. Don't retry the same request; it will fail again.
* **`X-Terra-RateLimit-Rule: r2`** — your code has issued requests for this user totalling more than 6,000 days in the current hour. Wait `Retry-After` seconds, then resume. Smaller windows won't help until the bucket resets.

A simple back-off:

```python
import time, requests

while True:
    r = requests.get(url, headers=headers)
    if r.status_code != 429:
        break
    if r.headers.get("X-Terra-RateLimit-Rule") == "r1":
        raise ValueError("window > 5 years; chunk it")
    time.sleep(int(r.headers.get("Retry-After", 60)))
```

### Best practices

* **Cache** what you've already fetched. The cumulative cap is wall-clock time, not cache misses — replays count too.
* **Chunk backfills.** A 5-year backfill at 30-day chunks is 60 calls; spread them across a few hours and you'll never see a 429.
* **Watch the `X-Terra-RateLimit-Remaining` header** in production logs. Drops toward zero predict throttling before it happens.
* **One request per user at a time.** Parallel calls for the same user share a budget; concurrency does not multiply your allowance.

{% hint style="warning" %}
**Connections without a user (e.g. before authentication completes) aren't rate-limited.** Once a connection ID exists, the budget starts.
{% endhint %}

### FAQ

**Does this apply to webhook deliveries?** No. Rate limits only apply to inbound API requests on the data-read endpoints listed above.

**Does the budget reset at the top of the hour or rolling from my first request?** Fixed buckets aligned to UTC (e.g. 14:00–15:00 UTC). The `Retry-After` header tells you exactly when the current bucket resets.

**Can I get a higher limit?** Reach out to your account contact — limits can be raised per `dev_id` for legitimate use cases.

**What counts as one "day" of cost?** Whatever you pass as `end_date − start_date`. A request like `?start_date=2024-01-01&end_date=2024-01-31` costs 30 days, regardless of how much data the user actually has in that range.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.tryterra.co/reference/health-and-fitness-api/rate-limits.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
