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

# Rate limits

> QWED API rate limiting by plan tier. Learn about Free, Pro, and Enterprise quotas, rate limit headers, and how to handle 429 Too Many Requests responses.

API rate limiting and quotas.

## Default limits

| Plan | Requests/min | Requests/day | Batch size |
| - | - | - | - |
| **Free** | 60 | 1,000 | 10 |
| **Pro** | 600 | 50,000 | 50 |
| **Enterprise** | Unlimited | Unlimited | 100 |

## Rate limit headers

Every response includes rate limit headers:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1703073600
```

| Header | Description |
| - | - |
| `X-RateLimit-Limit` | Max requests per window |
| `X-RateLimit-Remaining` | Requests remaining |
| `X-RateLimit-Reset` | Unix timestamp of reset |

## Rate limit response

When rate limited, you'll receive:

```json theme={null}
{
  "error": {
    "code": "QWED-005",
    "message": "Rate limit exceeded",
    "details": {
      "limit": 60,
      "reset_at": "2024-12-20T12:01:00Z",
      "retry_after": 45
    }
  }
}
```

HTTP Status: `429 Too Many Requests`

## Best practices

### 1. Implement exponential backoff

```python theme={null}
import time

def verify_with_retry(client, query, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.verify(query)
        except RateLimitError as e:
            wait = min(2 ** attempt, 60)
            time.sleep(wait)
    raise Exception("Max retries exceeded")
```

### 2. Use batch endpoints

Instead of individual requests:

```python theme={null}
# ❌ 10 requests
for item in items:
    client.verify(item)

# ✅ 1 request
client.verify_batch(items)
```

### 3. Cache results

```python theme={null}
import hashlib

cache = {}

def cached_verify(client, query):
    key = hashlib.sha256(query.encode()).hexdigest()
    if key in cache:
        return cache[key]
    result = client.verify(query)
    cache[key] = result
    return result
```

## Per-endpoint limits

Some endpoints have specific limits:

| Endpoint | Limit |
| - | - |
| `/verify/batch` | 100 items/request |
| `/verify/consensus` | Per-tenant rate limit (same as default plan limits) |
| `/agent/register` | 10/hour |
| `/attestation/verify` | 1000/hour |

## Request size and time limits

Beyond request-count quotas, the API enforces hard size and wall-clock bounds on expensive operations. These limits are fixed and apply to every plan tier.

### `/verify/stats` upload limits

Statistical verification accepts a CSV upload, and both the transfer and the parsed dataset are capped:

| Limit | Value | Response when exceeded |
| - | - | - |
| Upload size | 10 MB | `413` with the byte limit in `detail` |
| Expanded dataset size | 1,000,000 cells (rows × columns) | `413` with the cell limit in `detail` |
| Body read deadline | 30 seconds | `408 Request Timeout` |
| Concurrent uploads (per process) | 8 in flight | `503` with `"Upload capacity reached — retry shortly."` |

The byte cap is enforced while the body is received, before any parsing starts, so chunked uploads without a `Content-Length` header are bounded too. The cell cap is enforced during parsing: QWED reads the CSV in chunks and aborts with `413` the moment the accumulated row × column count crosses the budget, so a compact but very wide file cannot allocate an oversized DataFrame. Empty or column-less CSVs return `400`.

On `503`, retry after a short delay. On `413`, reduce the file size or split the dataset — the limit cannot be raised per request.

### LLM provider call timeout

Every outbound LLM provider call (translation and codegen for natural-language verification) is pinned to a 30-second HTTP timeout with SDK retries disabled. A stalled provider surfaces as a bounded verification failure instead of holding the request open. Implement retries in your client if you need them; see [best practices](#best-practices).

### Consensus deadline

`/verify/consensus` runs all engines under a single 30-second aggregate deadline. Engines that miss the deadline are returned as explicit `BLOCKED` results rather than hanging the request or returning HTTP 500. See [Consensus engine — execution deadlines](/engines/consensus#execution-deadlines-and-partial-results).

### Math expression compute-cost bounds

Math expressions whose exact evaluation would be unboundedly expensive (huge integer literals, oversized exponents, power towers, large factorials) are rejected before evaluation. See [Math engine — compute-cost bounds](/engines/math#compute-cost-bounds).

## Per-IP limits on authentication endpoints

Anonymous `/auth/*` routes such as `POST /auth/signup` and `POST /auth/signin` carry no API key, so the per-key limiter cannot apply. As of v7.2, these routes are rate limited per client IP instead. This blocks password-guessing and prevents unauthenticated requests from saturating the service with expensive password hashing.

| Environment variable | Default | Description |
| - | - | - |
| `QWED_RATE_LIMIT_PER_IP` | `10` | Requests per minute per client IP on anonymous `/auth/*` routes. Must be at least 1; the server fails at startup otherwise. |
| `QWED_AUTH_TRUSTED_PROXIES` | empty | Comma-separated IPs or CIDR ranges of reverse proxies trusted to set `X-Forwarded-For`. |

When the limit is exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header (always at least 1 second):

```
HTTP/1.1 429 Too Many Requests
Retry-After: 42

{"detail": "Too many authentication attempts. Try again in 42 seconds."}
```

### Client IP resolution behind proxies

By default the direct peer address is the rate-limit key and the `X-Forwarded-For` header is ignored, so a client cannot mint fresh quotas by rotating spoofed header values. If QWED runs behind a load balancer or reverse proxy, set `QWED_AUTH_TRUSTED_PROXIES` to the proxy's address range:

```bash theme={null}
export QWED_AUTH_TRUSTED_PROXIES=172.16.0.0/12
```

When the direct peer matches a trusted proxy, QWED uses the rightmost `X-Forwarded-For` hop as the client IP. Trusted proxies append the real client address after any client-supplied entries, so clients still cannot choose their own bucket. Port suffixes (`1.2.3.4:8080`, `[2001:db8::1]:8080`) are stripped before bucketing.

<Note>
  Per-IP buckets are process-local. Running multiple workers or replicas multiplies the effective per-IP budget by the process count. Run a single replica for exact limits, or move rate limiting to a shared store such as Redis before scaling out.
</Note>

## Thread-safe in-memory limiter

The default in-memory rate limiter is thread-safe. All check-and-record operations (per-key and global) are protected by a lock, making it safe to use with multi-threaded ASGI servers such as Uvicorn with multiple workers. The `get_reset_time` method also operates under the lock to prevent stale reads.

## Fail-closed enforcement

When using Redis-backed rate limiting, the rate limiter operates with a **fail-closed** policy. If the Redis backend encounters an error at runtime, requests are denied rather than allowed through. This ensures that a temporary Redis outage does not silently bypass rate limits.

If Redis is unavailable at startup, an in-memory fallback is used until the service is restarted with a healthy Redis connection.

## Enterprise options

For higher limits, contact us for:

* Custom rate limits
* Dedicated infrastructure
* SLA guarantees


## Related topics

- [API overview](/api/overview.md)
- [Frequently asked questions](/faq.md)
- [Prompt injection defense and QWED security hardening](/advanced/security-hardening.md)
- [Zero-trust boundary for A2A agent communication](/a2a/trust-boundary.md)
- [QWED protocol specification](/specs/qwed-spec.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.