Rate limits

Rate limits

Paid applies per-organization rate limits to protect the platform for every customer. This page explains how limits are counted, how to recognize a rate-limited request, and how your integration should respond to a 429.

How limits are counted

Limits use a token bucket per organization: a bucket holds a budget of tokens (its burst capacity) and refills continuously at a sustained per-second rate. Short spikes above the sustained rate succeed as long as the bucket holds enough tokens.

Today the per-organization limit applies to signal ingestion (POST /api/v2/signals/bulk and the legacy v1 usage endpoints). Ingestion is signal-counted: a request carrying 500 signals consumes 500 tokens, not 1. Other endpoints are not yet rate limited per organization; as limits extend to them they will use this same framework, counting one token per request. Live and test-mode organizations have separate buckets, so load tests against a test organization never consume your production budget.

Limits are per organization, not per API key. If your workload needs a higher sustained rate, contact Paid support.

The 429 response

When the bucket cannot cover a request, the API responds with 429 Too Many Requests, a Retry-After header (in seconds), and an error body:

1{
2 "error": "Rate limit exceeded",
3 "code": "RATE_LIMIT_EXCEEDED",
4 "details": "Too many requests. Retry after 3s."
5}

The 429 status, the RATE_LIMIT_EXCEEDED code, and the Retry-After header are the complete contract: wait at least Retry-After seconds, then retry.

A rate-limited signals request is rejected whole: no signals from that request are ingested. Retry the whole batch after the Retry-After interval; a stable idempotencyKey on each signal makes the retry safe (see below).

Two kinds of 429

The API returns 429 for two distinct limits. Discriminate on the body code, not the status:

codeWhat it meansRetry signal
RATE_LIMIT_EXCEEDEDThe per-organization rate limit described on this pageRetry-After header (seconds)
CONCURRENCY_LIMIT_EXCEEDEDToo many analytics queries running at once for your organizationNo Retry-After; retry once a query finishes

Only the rate limiter’s 429 carries Retry-After. A CONCURRENCY_LIMIT_EXCEEDED response clears as soon as one of your in-flight analytics queries completes.

Back off correctly

  • Respect Retry-After. Wait at least that many seconds before retrying the request.
  • Use exponential backoff with jitter for repeated 429s. Never retry in a tight loop; that keeps your bucket empty and delays your own recovery.
  • Smooth your send rate rather than bursting: a steady stream at your working rate ingests everything a spiky one does, without rejections.
  • Batch signals up to the 500-per-request maximum. Fewer, fuller requests reduce overhead, and the signal-counted cost is the same.
  • Give every signal a stable idempotencyKey, and resend exactly the same key on a retry — never generate a fresh key per attempt. Keys are optional, and signals without one are not deduplicated, so retrying a keyless batch can ingest the same signals twice. Duplicate keys within an organization are skipped, which is what makes whole-batch retries safe.