Rate limits
Limits are enforced per organization, not per key. Money is per organization, so a customer with five keys gets one budget rather than five. Per-key sub-limits exist so one runaway service cannot starve another; they default to the organization limit and can never raise it.
Every response, successful or not, carries the current state of the meters. You should never have to guess.
The published limits
Section titled “The published limits”These are the paid defaults. Your account’s actual values are in GET /v1/account and in the
headers on every response; free-tier values are lower and are listed under
Pricing.
| Meter | Default | Denied with |
|---|---|---|
| Requests per minute | 120 | 429 rate_limit_requests |
| Pages per minute | 12,000 | 429 rate_limit_pages |
| Concurrent requests | 4 | 429 rate_limit_concurrency |
| Upload bytes per minute | 512 MiB | 429 rate_limit_uploads |
/v1/inspect per minute | 30 | 429 rate_limit_inspect |
/v1/inspect scan: "sample" pages per day | 2,000 | 429 rate_limit_inspect |
/v1/toc per minute | 60 | 429 rate_limit_toc |
| Zero-value conversions per hour | 60 | 429 rate_limit_no_value_conversions |
| Unbilled compute per hour | 60,000 vCPU-ms | 429 rate_limit_unbilled_work |
And the request-shape limits, which are not meters but will also stop you:
| Limit | Value |
|---|---|
Maximum upload, /v1/convert | 32 MiB |
Maximum upload, /v1/files | 128 MiB |
| Maximum pages per document | 5,000 on a paid account |
Maximum selected pages, /v1/convert | 200 |
| Request wall clock | 60 s hard |
| Total request-body deadline | 20 s from the first byte |
pages expression | 2,048 characters, 1,000 ranges |
| Idempotency window | 25 hours |
| File retention | 24 h default, 7 d maximum |
| Result cache TTL | 24 hours |
The headers
Section titled “The headers”x-request-id: req_01JQ8Z5T7B9KX2W4M6N0P3R5S7x-kaho-pages-processed: 5x-kaho-pages-billed: 5x-credits-charged: 5x-credits-balance: 219995x-ratelimit-limit-requests: 120x-ratelimit-remaining-requests: 118x-ratelimit-reset-requests: 2026-08-17T18:23:00Zx-ratelimit-limit-pages: 12000x-ratelimit-remaining-pages: 11688x-ratelimit-reset-pages: 2026-08-17T18:23:00Zx-ratelimit-limit-concurrency: 4x-ratelimit-remaining-concurrency: 3x-ratelimit-scope: orgretry-after: 2The names follow the convention every HTTP client library and observability vendor already parses.
reset is an RFC 3339 UTC timestamp, not a duration. It parses with Date.parse,
time.RFC3339 and datetime.fromisoformat with no clock arithmetic on your side. It means the
moment the meter is fully replenished — which a fixed-window counter cannot compute correctly, and
which the leaky-bucket meter we actually use produces for free.
retry-after appears on 429 and 503 only.
The burst refills continuously
Section titled “The burst refills continuously”There is no minute boundary and no thundering-herd moment. The limiter tracks a single value per meter and refills smoothly.
Worked example, with x-ratelimit-limit-requests: 120 and a burst allowance of 40. The emission
interval is 60,000 ms ÷ 120 = 500 ms.
- From idle you may fire 40 requests instantly.
- After that you are admitted at one every 500 ms.
- Go idle for 10 seconds and you have re-earned 20 of the burst.
Pacing against this is simple: read x-ratelimit-remaining-requests, and when it approaches zero,
space your calls at the emission interval rather than waiting for a window to roll over.
Admission versus settlement
Section titled “Admission versus settlement”Two meters, metered at two different moments, and the asymmetry is deliberate.
Requests per minute is metered on admission. The cost is 1 and it is known in advance.
Pages per minute is metered on settlement, after the conversion finishes and the real page
count is known. Reserving the endpoint ceiling on admission would meter someone converting
three-page invoices as though they were converting two-hundred-page documents, producing 429s at
a small fraction of their real limit.
A consequence worth planning for: x-ratelimit-remaining-pages reflects the state after this
request settled, which is exactly what you need for pacing the next one.
The page meter counts work, not revenue
Section titled “The page meter counts work, not revenue”The pages-per-minute meter advances on pages_processed, not pages_billed, on every terminal
outcome — including 4xx and 5xx.
The contractual promise is unchanged: errors never cost you a credit. What has changed is that failing repeatedly is no longer free of rate. A request that loads nineteen pages, converts one and errors out consumed nineteen pages of our capacity, and the meter sees nineteen.
Alongside it, an hourly budget covers compute consumed by requests that settled zero credits,
denied with 429 rate_limit_unbilled_work. It is 60,000 vCPU-milliseconds per hour — a full minute
of dedicated compute given away free every hour — which no legitimate workload reaches and which no
retry loop can drain.
Both meters exist for the same reason: price is per page billed, cost is per page processed, and a meter keyed on billing would see none of the work.
What to do about each 429
Section titled “What to do about each 429”| Code | What it means | What to do |
|---|---|---|
rate_limit_requests | Too many calls per minute | Pace at the emission interval; honour Retry-After |
rate_limit_pages | Too many pages per minute | Convert fewer pages per call, or spread the batch out |
rate_limit_concurrency | More than 4 requests in flight for your organization | Cap your worker pool at your published concurrency |
rate_limit_uploads | Too many bytes per minute | Upload once with POST /v1/files and convert from the file_id |
rate_limit_inspect | Too many probes, or the daily sample page budget is out | Cache inspection results; a document’s structure does not change |
rate_limit_toc | Too many /v1/toc calls per minute | Cache the table of contents alongside the document |
rate_limit_no_value_conversions | Too many conversions produced nothing at all | You are probably converting scanned documents. There is no OCR in v1 — check with /v1/inspect first |
rate_limit_unbilled_work | Too much compute spent on requests that billed zero | Stop retrying failures immediately; fix the input |
rate_limit_new_account | The first-week burn cap of 20,000 credits per 24 hours | Wait for the automatic lift, or ask support |
Concurrency is a lease, not a rate
Section titled “Concurrency is a lease, not a rate”Concurrency is not metered over time; a slot is taken when your request starts and released when it finishes. Slots expire automatically at the endpoint’s maximum wall clock, so a crashed client cannot leak one forever.
The published concurrency is 4, everywhere. Size your worker pool to it. Running eight workers
against a limit of four does not go twice as fast; it produces 429 rate_limit_concurrency on half
of them.
503 overloaded is not a rate limit
Section titled “503 overloaded is not a rate limit”429 rate_limit_concurrency means your organization exceeded its own limit and the fix is yours.
503 overloaded means the fleet is saturated and the fix is to back off — it carries Retry-After
and retryable: true.
A retry policy that works
Section titled “A retry policy that works”- Retry only on
429,500,502,503,504, or whenerror.retryableistrue. - Honour
Retry-Afterwhen present. It is not a suggestion. - Otherwise use exponential backoff with full jitter — a random wait between zero and the current ceiling. Synchronised retries are how a recovered service goes down again.
- Never retry a POST without an
Idempotency-Key. Without one, a retried conversion is a second conversion at full price. - Cap total attempts. A
4xxthat is not a429will fail identically forever.
curl -sS -D - -o /dev/null https://api.kaho.ai/v1/account \ -H "Authorization: Bearer $KAHO_API_KEY" \ | grep -i '^x-ratelimit'