# Rate & concurrency limits

Two kinds of limits keep the platform fast and fair: a **rate limit** on how many requests your workspace sends per minute, and a **concurrency limit** on how many valuations, selling-time forecasts and normalizations run at the same time. This page explains both, how to read them and how to back off.

## Rate limits

Each workspace has three per-minute buckets, each with its own counter:

| Bucket | Default | Counts |
| --- | --- | --- |
| Valuations | 60 requests per minute | `POST /v1/valuations` and `POST /v1/selling-times` (including replays), `GET /v1/valuations/{id}`, `GET /v1/account`, and the MCP `valuate_car`, `forecast_selling_time`, `get_valuation` and `get_account` tools |
| Vehicles | 60 requests per minute | `POST /v1/vehicles/normalize`, `/decode`, `/hsn-tsn` and `/lookup`, and the MCP `normalize_vehicle`, `decode_vin`, `decode_hsn_tsn` and `lookup_plate` tools |
| Catalogue | 120 requests per minute | Every `GET /v1/catalogue/...` endpoint, the MCP `search_vehicles` tool and the console's catalogue browser |

The valuations and vehicles buckets share one setting, `rate_limit_per_minute`, but are counted separately: 60 valuation requests and 60 vehicle requests can run in the same minute. A selling-time forecast is a valuation with a forecast added, so `POST /v1/selling-times` spends the **valuations** bucket and needs the same [concurrency slots](#concurrency-limits) as a valuation.

Test-key requests that identify a vehicle (valuations, selling-time forecasts, normalization and VIN, HSN/TSN and plate lookups) also share a **fair-use allowance** per workspace: 10 per minute, 100 per hour and 300 per day. When a window is used up they are refused with `429 test_limit_reached` and `retry_after`, the seconds until that window resets. See [testing](/docs/guides/testing#test-keys-are-for-testing).

The limits apply to the whole workspace, across all its keys, REST and MCP. `GET /v1/account` returns `rate_limit_per_minute`, the limit of the valuations bucket and of the vehicles bucket. AutoUncle can raise the limits for your workspace: [contact sales](/contact-sales).

Windows are fixed calendar minutes: a counter starts at zero at the start of each minute. Every rate-limited response carries headers:

| Header | Meaning |
| --- | --- |
| `RateLimit-Limit` | Requests allowed in the current minute for this bucket. |
| `RateLimit-Remaining` | Requests left in the current minute. |
| `RateLimit-Reset` | Seconds until the next minute starts. |

When a bucket is empty, the request is rejected with `429 rate_limited` before anything is recorded or charged. The Problem has `retry_after`, the seconds until the window resets, and REST also sends it as the `Retry-After` header. Wait that long, then send the same request with the same idempotency key.

```json
{
  "type": "https://data-services.autouncle.com/docs/errors#rate_limited",
  "title": "Rate limited",
  "status": 429,
  "code": "rate_limited",
  "detail": "The request rate limit was reached. Retry after the current minute.",
  "errors": [],
  "fix": ["Wait for the rate limit to reset, then retry the same request with its original idempotency key."],
  "request_id": "3f0f8c3e-6a1e-4d39-9f53-1f7f0b8d2c11",
  "docs_url": "https://data-services.autouncle.com/docs/errors",
  "billable": false,
  "retryable": true,
  "retry_after": 37
}
```

## Concurrency limits

Valuations, selling-time forecasts and normalizations also need a free slot while they run, from one shared pool: a forecast and a valuation compete for the same slots. VIN, HSN/TSN and plate lookups do not need one. Live requests need a slot at three levels at once:

| Scope | Default | When it is full |
| --- | --- | --- |
| Your workspace (live keys) | 4 valuations at once | `429 concurrency_limited` |
| The market (for example all DK traffic) | 16 at once | `503 engine_busy` |
| The whole platform | 128 at once | `503 engine_busy` |

Test-key valuations never reach the live scorer. They use a separate limit of **4 at once per workspace**, so your tests behave like production but never block live traffic. AutoUncle tunes the market and platform limits to the scorer capacity, so they may differ from these defaults.

When no slot is free, the request waits up to about **two seconds** for one. If none frees up, it is rejected before anything is reserved, charged or recorded. Both rejections are retryable and carry `retry_after` (2 seconds by default) in the body and the `Retry-After` header on REST. Retry with the **same** idempotency key and body. Finished replays do not need a slot.

`concurrency_limited` means your own integration is sending too many valuations in parallel: lower your concurrency. `engine_busy` means the market or the platform is busy: back off and retry.

## When limiting is unavailable

The limits are enforced with a shared store. If it is unavailable, requests fail closed with `503 rate_limit_unavailable` rather than running unlimited. It is retryable and was not started, so retry the same request with the same key after a short delay.

## Size limits

| Limit | Value |
| --- | --- |
| Request body | 1 MB, including inline images. Larger requests return `413 payload_too_large`. |
| `description` | 20,000 characters |
| `images` | 1 to 8 per request; remote images up to 8 MB each |
| `vehicle.equipment` | 150 keys |
| `vehicle.flags` | 30 entries |
| `Idempotency-Key` | 200 characters |

## Staying within the limits

- Use a bounded worker pool: at most 4 concurrent live valuations per workspace by default.
- Pace with the headers: when `RateLimit-Remaining` reaches 0, sleep for `RateLimit-Reset` seconds.
- Always honor `retry_after`, add a little random jitter, and cap the number of retries.
- Cache catalogue lists rather than fetching them for every valuation; see [catalogue caching](/docs/catalogue#versions-and-caching).
- Keep batch jobs and interactive traffic apart, or let batches yield when interactive traffic is high.

[Batch valuations](/docs/guides/batch-valuations) shows a complete worker loop.

## Over MCP

MCP tool calls share the same buckets and slots as REST. MCP has no response headers, so the delay is only in the tool result's `retry_after` field, inside a result with `isError: true`. `get_account` and `get_valuation` count against the valuation bucket exactly like `GET /v1/account` and `GET /v1/valuations/{id}`, and return the same `rate_limited` Problem with `retry_after` when it is empty.
