> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/reference/health-and-fitness-api/rate-limits.md).

# 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.
