# Rate limits

What each request costs, the ceilings per token and per organisation, and the headers that tell you how much budget is left.

Every request to `/api/v2` spends units from rate buckets. You can work out what a request costs before you send it, and every response tells you how much budget is left.

## Ceilings

Every bucket counts over a window of 60 seconds.

| Bucket                                               | Read units | Write units |
| ---------------------------------------------------- | ---------- | ----------- |
| Per token                                            | 600        | 120         |
| Per organisation, shared by all its tokens and users | 3000       | 600         |

- Each token has its own budget, but every token in your organisation also spends from the organisation's bucket. Another token can therefore lower what you have left.
- For an app that signs in with OAuth, the per-token budget belongs to the app and the user who signed in. It carries over when the access token is refreshed.

Before the token is checked, a per-IP limit applies as well: 600 requests per minute per IP address and host, and 1200 requests per minute per IP address. It counts requests, not units, and it never shows in the `RateLimit` headers.

These ceilings are the current budget. Read `RateLimit-Policy` at runtime instead of hard-coding them.

## Read and write buckets

- A `GET` spends its cost from the read buckets.
- A `POST`, `PATCH` or `DELETE` spends its cost from the write buckets, plus 1 unit from the read buckets. A write therefore needs room in both, and an empty read bucket blocks writes too.

Each operation page in the reference shows the bucket it spends from.

## What a request costs

```text
cost = base                  1, or 3 for view=detail on a collection
     + each include path     +1, or +3 when the path is a list
     + q                     +2 when a search term is sent
     + with_total=1          +2
```

| Part                    | Units          | When it applies                                                                                  |
| ----------------------- | -------------- | ------------------------------------------------------------------------------------------------ |
| Base                    | 1              | Every request.                                                                                   |
| Base with `view=detail` | 3 instead of 1 | Only on a collection read. On a single record and on a write, `view=detail` costs nothing extra. |
| Include path, to-one    | +1 per path    | The path inlines one related record, such as `assignee`.                                         |
| Include path, to-many   | +3 per path    | The path inlines a list of records, such as `labels`: up to 25, or 50 for `tags`.                |
| `q`                     | +2             | A search term is sent.                                                                           |
| `with_total=1`          | +2             | A collection read asks for the total.                                                            |

A `DELETE` always costs 1.

Name each include path once. A path you repeat, such as `include=labels,labels`, is loaded once but charged every time it appears.

### Worked example

`GET /work-items` takes the to-many include paths `comments`, `drive_items`, `labels` and `subtasks`, and to-one paths such as `assignee` and `sprint`.

| Request                                                                                     | Cost                       |
| ------------------------------------------------------------------------------------------- | -------------------------- |
| `GET /work-items`                                                                           | 1                          |
| `?view=detail`                                                                              | 3                          |
| `?include=assignee`                                                                         | 1 + 1 = 2                  |
| `?include=labels`                                                                           | 1 + 3 = 4                  |
| `?include=assignee,sprint,labels`                                                           | 1 + 1 + 1 + 3 = 6          |
| `?q=invoice&with_total=1`                                                                   | 1 + 2 + 2 = 5              |
| `?view=detail&q=invoice&with_total=1&include=comments,drive_items,labels,subtasks,assignee` | 3 + 2 + 2 + 4 × 3 + 1 = 20 |

With each path named once, the last request is as expensive as `GET /work-items` gets: it names as many include paths as one request may, and every to-many path is among them. [Limits at a glance](https://developers.plnnrly.com/en/docs/api/v2/guides/limits/) has the maximum.

### What costs exactly one unit, and what costs nothing

The extra units are charged only on a successful `2xx` response that was not replayed. So these cost exactly 1 unit, whatever the request asked for:

- a `304 Not Modified`
- a replayed `POST` that returns the stored response for its `Idempotency-Key`
- an error response, such as a `400` or a `422`

These cost no units at all:

- a `429`
- a path or method that does not exist
- a request refused before or at the token check: an address that serves no API, a switched-off REST API, or a missing or invalid token. It still counts towards the per-IP limit.

## The headers

Every response carries the current state of your budget, errors and `429` included:

```http
RateLimit: limit=600, remaining=598, reset=42
RateLimit-Policy: 600;w=60
```

| Header             | Means                                                                                                                                                                                                                           |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RateLimit`        | `limit` is the ceiling of the tightest bucket this request spent from: the one with the fewest units left. `remaining` is what is left in it, after this request's own cost. `reset` is the number of seconds until it refills. |
| `RateLimit-Policy` | The same bucket's ceiling and window in seconds, as `<limit>;w=<seconds>`.                                                                                                                                                      |

These headers use the syntax of an earlier draft of the IETF RateLimit header fields. Later drafts changed the syntax, so parse the form shown here.

Three older headers are still sent, but they are deprecated and stop on 2027-09-01:

- `X-RateLimit-Limit`
- `X-RateLimit-Remaining`
- `X-RateLimit-Reset`

Move to `RateLimit` now. Note that `X-RateLimit-Reset` is a Unix timestamp, while the `reset` in `RateLimit` counts seconds from now.

## When you run out

You get `429` [rate-limit-exceeded](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#rate-limit-exceeded):

```json
{
  "type": "https://developers.plnnrly.com/api/v2/problems/rate-limit-exceeded",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "The rate limit for this caller has been exhausted. Retry after the interval in the Retry-After header.",
  "instance": "/api/v2/work-items",
  "request_id": "01K4B7Q9YV3PZ2N8T1E6M0XR4S",
  "rate_limit": {
    "limit": 600,
    "retry_after": 42
  }
}
```

`rate_limit.limit` is the ceiling that ran out. `rate_limit.retry_after` and the `Retry-After` header give the seconds until it refills. Wait that long: a retry that arrives earlier is refused again.

A `429` is always safe to retry, because the request did not run. On a `POST`, retry with the same [Idempotency-Key](https://developers.plnnrly.com/en/docs/api/v2/guides/idempotency/) anyway.

## MCP and OAuth

- **MCP.** Calls through the MCP server do not spend these units. The MCP server allows 60 requests per minute per user, and every MCP request counts, not only tool calls. See [Tools](https://developers.plnnrly.com/en/docs/mcp/tools/).
- **OAuth.** Registering an app and getting or refreshing its tokens have limits of their own. See [Sign in with OAuth](https://developers.plnnrly.com/en/docs/api/v2/guides/oauth-apps/).

## Spending less

- **Ask for full pages.** Send `per_page=100`: a page costs the same whether it holds 1 row or 100. With a to-many include, the maximum is 25 rows per page.
- **Do not ask for a total you will not read.** `with_total=1` adds 2 units, and `links.next` already tells you whether there is another page.
- **Prefer an include over follow-up reads.** A to-one path adds 1 unit for a whole page of rows. A to-many path adds 3 and limits the page to 25 rows, so compare it with reading that list separately.
- **Stay on the summary view while paging.** `view=detail` triples the cost of a collection read.
- **Honour the cache lifetime.** A few configuration reads, such as labels and work types, declare `Cache-Control: private, max-age=60`, and discovery declares `private, max-age=300`. The operation page says so. Re-reading them sooner spends units for nothing.
- **Revalidate instead of reading again.** Send `If-None-Match`: a `304` costs 1 unit, even for a request that would otherwise cost more. See [Concurrency and ETags](https://developers.plnnrly.com/en/docs/api/v2/guides/concurrency-and-etags/).

## Related

- [Errors and retries](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/)
- [Limits at a glance](https://developers.plnnrly.com/en/docs/api/v2/guides/limits/)
- [Keeping a copy in sync](https://developers.plnnrly.com/en/docs/api/v2/guides/syncing-data/)
- [Headers and versioning](https://developers.plnnrly.com/en/docs/api/v2/guides/headers-and-versioning/)
