Skip to content

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.

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.

MeterDefaultDenied with
Requests per minute120429 rate_limit_requests
Pages per minute12,000429 rate_limit_pages
Concurrent requests4429 rate_limit_concurrency
Upload bytes per minute512 MiB429 rate_limit_uploads
/v1/inspect per minute30429 rate_limit_inspect
/v1/inspect scan: "sample" pages per day2,000429 rate_limit_inspect
/v1/toc per minute60429 rate_limit_toc
Zero-value conversions per hour60429 rate_limit_no_value_conversions
Unbilled compute per hour60,000 vCPU-ms429 rate_limit_unbilled_work

And the request-shape limits, which are not meters but will also stop you:

LimitValue
Maximum upload, /v1/convert32 MiB
Maximum upload, /v1/files128 MiB
Maximum pages per document5,000 on a paid account
Maximum selected pages, /v1/convert200
Request wall clock60 s hard
Total request-body deadline20 s from the first byte
pages expression2,048 characters, 1,000 ranges
Idempotency window25 hours
File retention24 h default, 7 d maximum
Result cache TTL24 hours
On every response, including 4xx and 5xx
x-request-id: req_01JQ8Z5T7B9KX2W4M6N0P3R5S7
x-kaho-pages-processed: 5
x-kaho-pages-billed: 5
x-credits-charged: 5
x-credits-balance: 219995
x-ratelimit-limit-requests: 120
x-ratelimit-remaining-requests: 118
x-ratelimit-reset-requests: 2026-08-17T18:23:00Z
x-ratelimit-limit-pages: 12000
x-ratelimit-remaining-pages: 11688
x-ratelimit-reset-pages: 2026-08-17T18:23:00Z
x-ratelimit-limit-concurrency: 4
x-ratelimit-remaining-concurrency: 3
x-ratelimit-scope: org
retry-after: 2

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

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.

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

CodeWhat it meansWhat to do
rate_limit_requestsToo many calls per minutePace at the emission interval; honour Retry-After
rate_limit_pagesToo many pages per minuteConvert fewer pages per call, or spread the batch out
rate_limit_concurrencyMore than 4 requests in flight for your organizationCap your worker pool at your published concurrency
rate_limit_uploadsToo many bytes per minuteUpload once with POST /v1/files and convert from the file_id
rate_limit_inspectToo many probes, or the daily sample page budget is outCache inspection results; a document’s structure does not change
rate_limit_tocToo many /v1/toc calls per minuteCache the table of contents alongside the document
rate_limit_no_value_conversionsToo many conversions produced nothing at allYou are probably converting scanned documents. There is no OCR in v1 — check with /v1/inspect first
rate_limit_unbilled_workToo much compute spent on requests that billed zeroStop retrying failures immediately; fix the input
rate_limit_new_accountThe first-week burn cap of 20,000 credits per 24 hoursWait for the automatic lift, or ask support

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.

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.

  1. Retry only on 429, 500, 502, 503, 504, or when error.retryable is true.
  2. Honour Retry-After when present. It is not a suggestion.
  3. 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.
  4. Never retry a POST without an Idempotency-Key. Without one, a retried conversion is a second conversion at full price.
  5. Cap total attempts. A 4xx that is not a 429 will fail identically forever.
Read the meters without spending anything
curl -sS -D - -o /dev/null https://api.kaho.ai/v1/account \
-H "Authorization: Bearer $KAHO_API_KEY" \
| grep -i '^x-ratelimit'