FurniOpsDocs

Rate limits

Budgets by rate class and tier, the IETF RateLimit headers that state them, what a 429 and a 503 overloaded mean, and how to back off.

FurniOps budgets requests so that one busy integration cannot slow everyone else. Every response says where you stand, so a well-behaved client never needs to guess.

How budgets work

  • Your organisation has one budget per mode and rate class: live and test are counted apart, and so are reads, writes and the rest. All your keys share it.
  • A budget is a token bucket: a burst you can spend at once, refilled at a steady rate. Spending the burst is fine; sustained traffic above the rate is not.
  • Every operation in this API needs a key, so every request spends your organisation's budget, never a shared one.

Rate classes

Every operation is in one class, named in the API reference as x-furniops-rate-class:

ClassWhatCounted
readReads: GET of objects, lists and the change feedper request
writeChangesper request
expensiveWork that fetches from a vendor or reprocesses data: runs and replays, source tests, stock checks, completing an upload, confirming mappings and category rulesper request
exportStarting a bulk export, budgeted per minuteper export started
streamOpening GET /v1/streamper connection; a reconnection counts
ClassOperations
read
write
expensive
export
stream

Budgets by tier

Your plan sets your organisation's rate tier. These are the current budgets; read the headers below rather than hard-coding them, as they can change.

Classstandardhighmax
read100/s, burst 200500/s, burst 1,0002,000/s, burst 4,000
write20/s, burst 40100/s, burst 200400/s, burst 800
expensive2/s, burst 510/s, burst 2040/s, burst 80
export1/min, burst 26/min, burst 1030/min, burst 60
stream1/s, burst 55/s, burst 2020/s, burst 50

An organisation without a plan yet gets standard. With a burst of 200 and 100 a second, a standard organisation can send 200 reads at once, then 100 a second for as long as it likes.

The headers

Every budgeted response carries the IETF RateLimit header fields (draft 11):

RateLimit-Policy: "standard.read";q=200;w=2
RateLimit: "standard.read";r=187;t=1
HeaderParameterMeaning
RateLimit-Policythe nameThe budget, <tier>.<class>.
qThe burst: how many requests a full budget holds.
wSeconds an empty budget takes to refill completely.
RateLimitrRequests left right now.
tSeconds until the budget is full again.

Retry-After (in seconds) is sent only when a request is refused. A budget counted per minute states its window in seconds too: "standard.export";q=2;w=120 is a burst of 2 refilled over two minutes.

headers.ts
const page = await furniops.products.list();
// RateLimit-Policy: "standard.read";q=200;w=2   (burst q, refill window w seconds)
// RateLimit:        "standard.read";r=199;t=1   (r requests left, t seconds until full)
console.log(page.lastResponse.headers.get("ratelimit-policy"));
console.log(page.lastResponse.headers.get("ratelimit"));

429 rate_limited

A request over budget is refused before it does anything, with 429 rate_limited, the headers above and Retry-After:

HTTP/2 429
Content-Type: application/problem+json
RateLimit-Policy: "standard.write";q=40;w=2
RateLimit: "standard.write";r=0;t=2
Retry-After: 1

{
  "type": "https://docs.furniops.com/errors/rate_limited",
  "title": "Too many requests",
  "status": 429,
  "code": "rate_limited",
  "detail": "The standard.write budget is spent. Retry after 1 s.",
  "request_id": "req_034XqBaL2mRLTZPXyRbaUl",
  "doc_url": "https://docs.furniops.com/errors/rate_limited"
}

Wait at least Retry-After seconds, then retry. Nothing was done, so retrying is always safe.

429 quota_exceeded is not a rate limit

A 429 with the code quota_exceeded means a monthly quota of your plan is spent, and its Retry-After counts down to the end of the month. Branch on code, not only the status. See Usage and quotas.

503 overloaded

Separately from your budget, a FurniOps server under heavy load sheds the least important requests first, so the rest keep being served. A shed request answers 503 overloaded with Retry-After of 2, 5 or 10 seconds, longer the heavier the load, and nothing was done.

PriorityRequestsShed
lowexport: starting a bulk exportfirst
mediumexpensive and streamnext
highread by organisations on the standard tierunder heavy load
criticalevery write, and read on the high and max tiersnever

Writes are never shed, so a change you make is never refused for load. Starting an export is the one exception: it is shed first, because it can wait.

Backing off

The SDK handles all of this: it retries 429 and every 5xx (503 overloaded included) after Retry-After when it is at most 60 seconds, and otherwise backs off exponentially from 0.5 to 8 seconds with jitter, twice by default. When Retry-After asks for longer than 60 seconds, or the retries run out, a 429 throws RateLimitError with retryAfterMs, so you can schedule the work for later:

later.ts
try {
  await furniops.sources.createRun("src_034XqBaL2mRLTZPXyRbaUc", {}, { maxRetries: 5 });
} catch (error) {
  // Thrown after the retries, or at once when Retry-After asks for more than 60 s.
  if (error instanceof RateLimitError) scheduleLater(error.retryAfterMs ?? 60_000);
  else throw error;
}

Without the SDK:

  1. On 429 or 503, wait Retry-After seconds. Without one, wait with exponential backoff and jitter (for example, a random time between half and all of 0.5 s, 1 s, 2 s, …, capped).
  2. Watch RateLimit's r. When it nears 0, slow down before you are refused.
  3. Spread scheduled jobs: many workers starting on the minute spend the burst together.

Spending less

  • Follow the change feed with wait=25 instead of polling objects.
  • Use limit=100 on lists, and up to 1000 on the change feed.
  • Send If-None-Match: an unchanged object answers 304 with no body.
  • Use webhooks: deliveries to you cost no budget.
  • Keep test traffic on test keys: it has its own budget and never eats into live.

On this page