The Future Demand API is rate-limited per partner. The exact numbers are not exposed in headers today; below is the practical guidance.

Defaults (as of this docs revision)

ScopeApprox budget
Per-partner reads~600 requests/min
Per-partner writes~120 requests/min
Auth (/auth, /auth/refresh_token)~10/min/IP
Polling (notification stats, package status)designed for 10s+ intervals
These are guidelines — Future Demand may adjust them per partner. If you have a legitimate need for higher throughput, contact your account contact.
Rate-limit responses do not currently include Retry-After or X-RateLimit-* headers. Treat any 429 as “back off and try again later.”

Detecting a rate limit

async function fdRequest<T>(path: string, init?: RequestInit): Promise<T> {
  const resp = await fetch(`${BASE}${path}`, init);
  if (resp.status === 429) {
    const retryAfter = Number(resp.headers.get("Retry-After")) || 5;
    throw Object.assign(new Error("rate_limited"), { status: 429, retryAfter });
  }
  if (!resp.ok) throw new Error(`${resp.status}`);
  return resp.json();
}
If Retry-After is missing (it often is), default to 5–10 seconds.

Exponential backoff with jitter

async function withBackoff<T>(
  fn: () => Promise<T>,
  { attempts = 5, baseMs = 500, maxMs = 30_000 } = {},
): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e: any) {
      if (e.status !== 429 && (e.status < 500 || e.status >= 600)) throw e;
      if (i === attempts - 1) throw e;

      const expo = Math.min(baseMs * 2 ** i, maxMs);
      const jitter = Math.random() * expo * 0.3;
      const delay = (e.retryAfter ? e.retryAfter * 1000 : expo) + jitter;
      await sleep(delay);
    }
  }
  throw new Error("unreachable");
}
The jitter matters when multiple clients (or multiple tabs) all hit the limit simultaneously — without it they retry in lockstep and trip the limiter again.

Client-side rate limiting

For workloads that walk many resources (e.g. “for each event, fetch the benchmark”), throttle yourself rather than rely on retry. Use a simple semaphore:
class Semaphore {
  private queue: Array<() => void> = [];
  constructor(private permits: number) {}

  async acquire() {
    if (this.permits > 0) { this.permits--; return; }
    await new Promise<void>(r => this.queue.push(r));
  }
  release() {
    this.permits++;
    const next = this.queue.shift();
    if (next) { this.permits--; next(); }
  }
}

const sem = new Semaphore(8);   // 8 concurrent calls

async function withConcurrency<T>(fn: () => Promise<T>): Promise<T> {
  await sem.acquire();
  try     { return await fn(); }
  finally { sem.release(); }
}

// usage
await Promise.all(events.map(e => withConcurrency(() => fetchBenchmark(e.id))));
8 concurrent calls is a reasonable default. Drop to 4 if you see 429s.

What gets rate-limited

Endpoint familyNotes
/auth/*The tightest budget. Don’t sign in on every request — cache the session.
Bulk reads (/events/, /campaigns/, …)Pagination is fine. Don’t ask for limit=10000.
Writes (/setup_processes/*, /campaigns_setup, …)Debounce autosaves (the reference webapp uses 1s debounce + AbortController).
Status pollsStay at 5s+ intervals.

Common mistakes

Tight loops hit the limiter and don’t make the task complete any faster. Use 2s with backoff.
A burst of failed requests creates a burst of refresh attempts. Use single-flight refresh (see Token refresh).
for (const e of events) await fetchDetail(e.id) looks innocent but sustained at 100ms-per-request hits the limit in 6 seconds. Use the semaphore pattern above.
POST /campaigns/, POST /setup_processes/{eid} are not idempotent. Retrying after a 429 is fine — but make sure the previous call really didn’t succeed (check status) before re-issuing the same write.