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

# NSP Rate Limiting: Limits, 429 Handling, and Config

> Per-endpoint rate limits enforced by NSP, the 429 response format, exponential backoff retry strategy, and axon.toml configuration options.

NSP enforces per-API-key rate limits to prevent agent code from overwhelming the daemon and the monitored applications. The rate limiter uses a **token bucket** algorithm — short bursts above the limit are absorbed by the bucket's capacity, but sustained request rates above the configured ceiling are throttled with a `429 Too Many Requests` response.

## Default Limits by Endpoint

Different endpoint categories carry different default limits, reflecting the relative cost of each operation.

| Endpoint                 | Method | Default Limit | Config Key                       |
| ------------------------ | ------ | ------------- | -------------------------------- |
| `/apps`                  | `GET`  | 100 req/s     | `rate_limit.state_per_second`    |
| `/state`                 | `GET`  | 100 req/s     | `rate_limit.state_per_second`    |
| `/schema`                | `GET`  | 100 req/s     | `rate_limit.state_per_second`    |
| `/diff`                  | `GET`  | 100 req/s     | `rate_limit.state_per_second`    |
| `/verify`                | `GET`  | 100 req/s     | `rate_limit.state_per_second`    |
| `/action`                | `POST` | 10 req/s      | `rate_limit.action_per_second`   |
| `/learn`                 | `POST` | 10 req/s      | `rate_limit.action_per_second`   |
| `/action` (`fresh=true`) | `POST` | 2 req/min     | `rate_limit.snapshot_per_minute` |
| `/health`                | `GET`  | No limit      | —                                |
| WebSocket streams        | —      | No limit      | —                                |

<Note>
  Rate limits are tracked per API key. If you run multiple agents sharing one key, their requests all count toward the same bucket. Use a dedicated key per agent to isolate rate limits.
</Note>

## The 429 Response

When you exceed a rate limit, the daemon returns `429 Too Many Requests` with a `Retry-After` header and a structured JSON body:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json
```

```json theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 1 second.",
    "retry_after_ms": 1000
  }
}
```

Always read `retry_after_ms` (or the `Retry-After` header in seconds) and wait at least that long before retrying. Sending requests immediately after a 429 wastes your token budget and prolongs the throttle window.

## Retry Strategy

Use exponential backoff when handling `429` responses. This prevents a flood of retries from cascading into further throttling.

<Tabs>
  <Tab title="Python SDK">
    The Python SDK raises `NSPRateLimitError` on a 429 response. The `retry_after` attribute contains the wait time in seconds.

    ```python theme={null}
    import asyncio
    import nelieo_nsp as nsp

    async def with_backoff(fn, max_retries: int = 5):
        """Call fn(), retrying with exponential backoff on rate limit errors."""
        for attempt in range(max_retries):
            try:
                return await fn()
            except nsp.NSPRateLimitError as e:
                if attempt == max_retries - 1:
                    raise
                # Honour the daemon's hint, but apply exponential backoff on top.
                wait = max(e.retry_after, 2 ** attempt)
                print(f"Rate limited — retrying in {wait}s (attempt {attempt + 1}/{max_retries})")
                await asyncio.sleep(wait)
    ```
  </Tab>

  <Tab title="curl / shell">
    Parse the `Retry-After` header and sleep before the next attempt:

    ```bash theme={null}
    MAX_RETRIES=5
    ATTEMPT=0

    until [ $ATTEMPT -ge $MAX_RETRIES ]; do
      RESPONSE=$(curl -s -w "\n%{http_code}" \
        -H "X-Substrate-Key: $NSP_API_KEY" \
        http://localhost:7842/substrate/v1/apps)

      STATUS=$(echo "$RESPONSE" | tail -1)

      if [ "$STATUS" -ne 429 ]; then
        echo "$RESPONSE" | head -1
        break
      fi

      WAIT=$(( 2 ** ATTEMPT ))
      echo "429 received — sleeping ${WAIT}s before retry $((ATTEMPT + 1))"
      sleep $WAIT
      ATTEMPT=$((ATTEMPT + 1))
    done
    ```
  </Tab>

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

    def get_apps(api_key: str, max_retries: int = 5) -> dict:
        headers = {"X-Substrate-Key": api_key}
        for attempt in range(max_retries):
            resp = requests.get(
                "http://localhost:7842/substrate/v1/apps",
                headers=headers,
                timeout=10,
            )
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 1))
                wait = max(retry_after, 2 ** attempt)
                print(f"Rate limited — retrying in {wait}s")
                time.sleep(wait)
                continue
            resp.raise_for_status()
            return resp.json()
        raise RuntimeError("Exceeded max retries after repeated 429 responses")
    ```
  </Tab>
</Tabs>

## How the Python SDK Surfaces Rate Limit Errors

`NSPRateLimitError` is raised any time the daemon responds with `429`. The exception carries the following fields:

```python theme={null}
try:
    result = await session.execute("mark_read", parameters={"id": "msg_001"})
except nsp.NSPRateLimitError as e:
    print(e.message)       # "Rate limit exceeded. Retry after 1 second."
    print(e.code)          # "rate_limited"
    print(e.retry_after)   # 1.0  (seconds as a float)
    print(e.status_code)   # 429
```

## Configuring Rate Limits in axon.toml

Adjust the defaults by editing the `[rate_limit]` section in `axon.toml`:

```toml theme={null}
[rate_limit]
enabled = true

# Read endpoints: GET /state, /apps, /schema, /diff, /verify
state_per_second = 100

# Write endpoints: POST /action, POST /learn
action_per_second = 10

# Heap-snapshot requests (POST /action with fresh=true) — expensive, keep low
snapshot_per_minute = 2

# How long to retain an idle token bucket before evicting it from memory
bucket_ttl_secs = 3600
```

<Info>
  Changes to `axon.toml` take effect on daemon restart. Unlike `axon_keys.json`, the rate limit configuration is not hot-reloaded.
</Info>

## Best Practices

<CardGroup cols={2}>
  <Card title="Use WebSocket streaming instead of polling" icon="arrows-spin">
    The `/watch` WebSocket stream is not rate-limited and delivers state-change events as they happen. Replace any tight `GET /state` polling loop with a WebSocket subscription to stay well within limits.
  </Card>

  <Card title="Batch state reads" icon="layer-group">
    A single `GET /state` call returns all state keys for an app. If your agent needs values from five fields, read them all in one request instead of five — every request counts against the same bucket.
  </Card>

  <Card title="Respect the Retry-After header" icon="clock">
    The daemon tells you exactly how long to wait. Honouring that value avoids wasting additional tokens on requests that will also be rejected.
  </Card>

  <Card title="Raise limits only for known workloads" icon="sliders">
    Increase `action_per_second` in `axon.toml` only when you have profiled your agent and confirmed it genuinely needs higher throughput — lower limits reduce blast radius if agent code loops unexpectedly.
  </Card>
</CardGroup>
