# Rate limits

> Per-endpoint-class rate limits, how to read 429 responses, and how to back off correctly.

Source: https://productos.ca/documentation/api/v1/rate-limits (last reviewed 2026-09-22)

ProductOS rate-limits per **user** (API tokens count against the user who issued them) per **endpoint class**. Limits are independent, exhausting writes doesn't affect reads.

## Limits by class

| Class | Limit | Endpoints |
| :--- | :--- | :--- |
| **List reads** | 120 / minute | `GET /api/v1/{entity}` |
| **Single reads** | 240 / minute | `GET /api/v1/{entity}/{id}`, `GET .../links/{junction}` |
| **Writes** | 60 / minute | `POST`, `PATCH`, `DELETE` on entity routes, `POST .../restore`, link and unlink calls |
| **Bulk** | 30 / minute | `POST /api/v1/{entity}/bulk` (each call is one hit, whatever its size) |
| **Export** | 6 / minute | `GET /api/v1/{entity}/export` |
| **AI** | Per route, 4 to 60 / minute (chat 20, ProductSense 10, embedding 10) | `POST /api/ai/*`, in-app session only |
| **Public widget submission** | 10 / minute per IP, 1,000 / day per key | `POST /api/v1/public/widget/feedback` |
| **Feedback API, secret key** | 120 / minute and 10,000 / day per key; read-backs and lists 300 / minute per key | `POST /api/v1/public/feedback`, `GET /api/v1/public/feedback`, `GET /api/v1/public/feedback/{id}` |
| **Feedback API, publishable key** | 10 / minute per IP, 1,000 / day per key | `POST /api/v1/public/feedback` |
| **MCP sign-in** | Registration 30 / hour per IP; token exchange and refresh 120 / minute per IP; revocation 60 / minute per IP | `POST /api/oauth/register`, `POST /api/oauth/token`, `POST /api/oauth/revoke` |

The same user calling from multiple devices/tokens shares the bucket, limits are *per user*, not per token. Different users in the same workspace have independent buckets.

## How limits work

Each class is a **fixed-window** bucket. The window opens on your first request and resets one minute later (one hour for MCP client registration, one day for the daily Feedback API caps); it is not aligned to wall-clock minutes. So if your first write lands at 12:00:30 and you make 60 writes in the next second, the 61st waits until 12:01:30.

## The 429 response

```json title="429 response"
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "details": {
      "retryAfterSeconds": 18
    }
  }
}
```

`details.retryAfterSeconds` is the number of seconds until the bucket resets; read it from the body. The Feedback API and the MCP sign-in endpoints also send it as a `Retry-After` header. There are no `X-RateLimit-*` headers.

## Backoff strategy

```typescript title="Respect Retry-After"
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof ApiError && err.code === "rate_limited" && i < attempts - 1) {
        const wait = ((err.details as { retryAfterSeconds?: number } | undefined)?.retryAfterSeconds ?? 1) * 1000;
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }
      throw err;
    }
  }
  throw new Error("unreachable");
}
```

For bulk operations, **stagger your requests** instead of relying on retries:

- Writing 1000 features? Send 50/min for 20 minutes, not 1000 in one burst.
- Reading 1000 rows? Use `limit=200` per request and 5 requests, not 1000 individual `GET /api/v1/.../{id}` calls.

## Bulk endpoints

For large updates and deletes, prefer the bulk endpoint:

```bash
POST /api/v1/feedback/bulk
{ "ids": ["FDBK-000042", "FDBK-000043"], "action": "update", "patch": { "status": "planned" } }
```

`action` is `update` or `delete` (a trash, where the entity has one), and `ids` takes up to 200 existing rows per call. Each call is **one** hit against its own 30 / minute bucket, regardless of payload size. Bulk does not create rows: for bulk creates, stagger your `POST`s under the writes limit.

## What's not rate-limited

- `GET /api/v1/health`: uncapped, intended for liveness probes.
- `GET /api/v1/openapi`: no per-user limit.
- Webhook inbound (e.g. `/api/integrations/github/webhook`), provider-side rate-limited by GitHub/Linear/etc., we don't double-cap.

## Upgrading limits

Enterprise customers can request higher limits per class via support. Self-serve limit adjustment isn't possible, limits exist to protect the platform and we want a conversation about the use case before lifting them.

## Next

- [Errors](https://productos.ca/documentation/api/v1/errors): full error code reference including `rate_limited`.
- [Pagination](https://productos.ca/documentation/api/v1/pagination): how to read large datasets without hitting list-read limits.
