# Rate limits (https://upapi.io/docs/rate-limits)



Two independent limits apply to every call.

**A per-minute burst limit** counts requests. It protects the platform from
spikes and resets on a rolling 60-second window.

**A monthly quota** counts *weighted units*, not requests. It is your actual
budget, and it resets at the start of each UTC calendar month.

Both are pooled across your whole organization. Creating more keys does not
create more headroom.

## Weighted units [#weighted-units]

Operations do not cost the same to run. A cached JSON lookup and a full search
scrape land very differently on our side, so each operation carries a
`unitWeight` and a call spends that many units.

Across everything you can call today, a single request spends between **1 and 20 units**.

| Units per call | What it covers |
| --- | --- |
| 1 | **Most operations** — A direct read: developer data, finance, news, weather, public profiles. More operations cost one unit than any other price. |
| 2–6 | **Parse & extract** — A real processing step on top of the fetch — PDF text, image OCR, page-to-Markdown, Reddit and Maps reads. |
| 10 | **Audio transcription** — GPU transcription, charged per call rather than per minute of audio. |
| 15 | **Social via residential IPs** — Instagram, TikTok and LinkedIn reads. The residential proxies they need are what make them costly. |
| 20 | **Browser rendering** — A real headless Chromium tab loading the page: screenshots and HTML-to-PDF. |

Most of the catalog sits in the cheapest band, and the category name is not
itself a signal — Bluesky and Mastodon cost the same as a weather lookup, while
Instagram costs fifteen times more. A plan's monthly budget therefore goes as
far as its unit count suggests only while you stay in that band.

Each operation's weight is published as data, so you can price a workload before
running it: the `metadata.unitWeight` field of each item in
[`/api/catalog`](https://app.upapi.io/api/catalog), and the `OPERATIONS` array
exported by the [TypeScript SDK](https://upapi.io/docs/sdk#inspecting-the-catalog). Every
operation's page in the marketplace also shows its cost in the spec row beside
the timeout and cache values.

<Callout title="Weights can exist that nothing you can call uses">
  The gateway's weighting rules also cover kinds of work that are not published to anyone today.
  Treat the table above as a description of the current catalog rather than a fixed schedule, and
  read the weight from the catalog rather than assuming it from a category.
</Callout>

## Response headers [#response-headers]

Every gateway response reports where you stand, so you never have to guess.

| Header                        | Meaning                                          |
| ----------------------------- | ------------------------------------------------ |
| `X-RateLimit-Limit`           | Requests allowed per minute on your plan         |
| `X-RateLimit-Remaining`       | Requests left in the current minute              |
| `X-RateLimit-Reset`           | Unix seconds at which the minute window resets   |
| `X-RateLimit-Limit-Month`     | Weighted units allowed this month                |
| `X-RateLimit-Remaining-Month` | Weighted units left this month                   |
| `X-Trial-Units-Limit`         | Total units included in your trial — trials only |
| `X-Trial-Units-Remaining`     | Trial units left — trials only                   |
| `Retry-After`                 | Seconds to wait, on most 429s — see below        |
| `x-request-id`                | Identifier for this call, on every response      |
| `x-upapi-cache`               | `HIT` when the response was served from cache    |

### The standard `RateLimit` fields [#the-standard-ratelimit-fields]

The same counters are also published in the IETF's standard form
([draft-ietf-httpapi-ratelimit-headers-11](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)),
so a client with a generic rate-limit layer can read them without knowing
anything about upAPI:

```http
RateLimit-Policy: "burst";q=10;w=60, "month";q=1000;w=2678400
RateLimit: "burst";r=9;t=57, "month";r=980;t=1209600
```

Both are structured-field lists with one member per named policy: `q` is the
quota per window, `w` the window in seconds, `r` what is left now and `t` the
seconds until it resets. `burst` counts requests; `month` and `trial` count
weighted units, the same numbers as the `X-RateLimit-*-Month` and
`X-Trial-Units-*` headers.

The `trial` policy appears only on a trial, exactly like its `X-` counterparts.
Hosted-function compute is deliberately **not** in this list — it is metered in
seconds of execution rather than in calls, so it would put a second currency
behind one `r`; it keeps the `X-Compute-*` headers.

The `X-RateLimit-*` headers are not deprecated and are not going away. Both sets
describe the same counters and are sent together; read whichever suits your
client.

The two trial headers are absent when you are not on a trial. Treat their
presence as the signal, rather than testing for a zero value. They are also
absent from a **burst-throttle** 429 even while a trial is live: that rejection
is decided before the trial budget is read, so it carries the minute and month
counters but not the trial pair. A 429 that names the trial cap does carry them,
with `X-Trial-Units-Remaining: 0`.

Where `Retry-After` is sent it is honest about which limit you hit: 60 seconds
for a burst throttle, the seconds remaining until the next UTC month for an
exhausted monthly quota, and the seconds until your trial ends for a spent trial
budget. Two 429s omit it — the hosted-function compute-quota rejection and a
`SERVICE_BUSY` from a saturated isolate pool — so back off on your own schedule
when it is missing rather than treating it as guaranteed.

## Which 429 you got [#which-429-you-got]

Do not infer the limit from the size of `Retry-After`. The
[error code](https://upapi.io/docs/errors#429-arrives-with-a-reason) says it outright:

| Limit                    | Code             | Waiting helps?                  |
| ------------------------ | ---------------- | ------------------------------- |
| Per-minute burst         | `RATE_LIMITED`   | Yes — 60 seconds                |
| Monthly weighted units   | `QUOTA_EXCEEDED` | Only at the next UTC month      |
| Trial unit budget        | `QUOTA_EXCEEDED` | Only when your paid plan starts |
| Hosted-function compute  | `QUOTA_EXCEEDED` | Only at the next UTC month      |
| Saturated execution pool | `SERVICE_BUSY`   | Yes — back off and retry        |

## Caching [#caching]

Most operations declare a cache TTL, and a cached response arrives with
`x-upapi-cache: HIT`. **A HIT costs zero units** — no operation ran, so there is
nothing to charge for — which makes a cacheable operation cheaper the more
repetitive your workload is. Sending a [custom proxy](https://upapi.io/docs/platform/proxies) disables
caching for that call, because a response fetched through your egress IP is not
interchangeable with one fetched through ours.

<Callout title="Absence of HIT is not proof of a miss">
  Marketplace operations emit `x-upapi-cache` only when the answer came from cache. Only your own
  hosted user APIs also emit an explicit `MISS`. Test for `=== 'HIT'`; never infer a miss from `===
    'MISS'`.
</Callout>

## What you are charged for [#what-you-are-charged-for]

You only pay for calls we actually ran. A request we reject before running it —
a validation error, an unknown operation, a bad key, a throttled call — costs
zero units, and so does an answer served from cache or replayed from an
`Idempotency-Key`.

| Outcome                                       | Units      |
| --------------------------------------------- | ---------- |
| The operation ran (any status it returned)    | Its weight |
| `400` / `404` / `401` — refused before it ran | 0          |
| `429` — throttled or out of quota             | 0          |
| `x-upapi-cache: HIT`                          | 0          |
| `Idempotency-Replayed: true`                  | 0          |

The one case worth reading twice: a `400` an **operation** returned is billed,
because reaching it means the operation ran and validated your input itself.
A `400` the gateway returned — a missing required field, a malformed body — is
not, because nothing upstream was contacted.

## One behaviour that surprises people [#one-behaviour-that-surprises-people]

### The minute window extends on every attempt, including rejected ones [#the-minute-window-extends-on-every-attempt-including-rejected-ones]

The per-minute counter's expiry is refreshed by every attempt, whether it was
served or throttled. Hammering a throttled key therefore keeps it throttled: the
window only clears after 60 seconds of complete silence on that key.

If you are draining a backlog, back off properly when you see a 429 instead of
retrying immediately, or spread the work across several keys and let each rest.

## Checking without calling [#checking-without-calling]

[`GET /api/usage`](https://upapi.io/docs/usage-api) reports your current minute, month and trial
position without spending anything.
