upAPI Docs

Rate limits

A per-minute burst limit, a monthly budget of weighted units, and the headers that report both.

View as Markdown

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

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 callWhat it covers
1Most operations — A direct read: developer data, finance, news, weather, public profiles. More operations cost one unit than any other price.
2–6Parse & extract — A real processing step on top of the fetch — PDF text, image OCR, page-to-Markdown, Reddit and Maps reads.
10Audio transcription — GPU transcription, charged per call rather than per minute of audio.
15Social via residential IPs — Instagram, TikTok and LinkedIn reads. The residential proxies they need are what make them costly.
20Browser 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, and the OPERATIONS array exported by the TypeScript SDK. Every operation's page in the marketplace also shows its cost in the spec row beside the timeout and cache values.

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.

Response headers

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

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute on your plan
X-RateLimit-RemainingRequests left in the current minute
X-RateLimit-ResetUnix seconds at which the minute window resets
X-RateLimit-Limit-MonthWeighted units allowed this month
X-RateLimit-Remaining-MonthWeighted units left this month
X-Trial-Units-LimitTotal units included in your trial — trials only
X-Trial-Units-RemainingTrial units left — trials only
Retry-AfterSeconds to wait, on most 429s — see below
x-request-idIdentifier for this call, on every response
x-upapi-cacheHIT when the response was served from cache

The standard RateLimit fields

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

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

Do not infer the limit from the size of Retry-After. The error code says it outright:

LimitCodeWaiting helps?
Per-minute burstRATE_LIMITEDYes — 60 seconds
Monthly weighted unitsQUOTA_EXCEEDEDOnly at the next UTC month
Trial unit budgetQUOTA_EXCEEDEDOnly when your paid plan starts
Hosted-function computeQUOTA_EXCEEDEDOnly at the next UTC month
Saturated execution poolSERVICE_BUSYYes — back off and retry

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 disables caching for that call, because a response fetched through your egress IP is not interchangeable with one fetched through ours.

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'.

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.

OutcomeUnits
The operation ran (any status it returned)Its weight
400 / 404 / 401 — refused before it ran0
429 — throttled or out of quota0
x-upapi-cache: HIT0
Idempotency-Replayed: true0

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

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

GET /api/usage reports your current minute, month and trial position without spending anything.