Concepts

Rate Limits

Per-tier quotas, sliding-window limits, and the 429 + upgrade_url contract.

brandRNA enforces three independent layers of rate-limiting:

  1. Per-tier monthly quota — your account's contractual call budget.
  2. Per-key sliding-window burst limit — a short-window guard against runaway loops.
  3. Per-IP demo limiter — only applies to the homepage demo widget, never to authenticated traffic.

Read on for the contract details.

Quotas by tier

TierMonthly callsBurst (per minute)Overage behaviour
Free10060Hard 429 with upgrade_url once monthly cap is hit.
PaidUsage-based300Soft — meter ticks up, billed via Stripe; user-set $-cap can pause.

See the pricing page for current per-call rates on the paid tier. Cached calls (cache-hit responses) are always billed at $0, regardless of tier.

Cached calls are free in every tier. The metadata.cached: true field on a brand-pack response is your signal that you didn't burn quota.

The 429 response

When the monthly cap (free tier) or per-minute burst is hit, the server returns:

{
  "error": "quota_exceeded",
  "code": "quota_exceeded",
  "details": {
    "tier": "free",
    "monthly_used": 100,
    "monthly_limit": 100,
    "upgrade_url": "https://brandrna.com/pricing"
  }
}

upgrade_url is the canonical deep-link to the upgrade flow. Surface it directly to your end-users — don't hard-code the URL in your client; we may rotate it for A/B tests.

The 429 response does not include a Retry-After header today. If you need server-driven retry timing, pin to the changelog — it'll ship in a future minor release.

Sliding-window mechanics

The burst limiter is a sliding window keyed by API key and remote IP, backed by a counter store (in-memory locally, Upstash Redis in production). The window is approximately 60 seconds; specifics:

  • Each request advances a counter under (api_key, ip) and (api_key).
  • When either counter exceeds its tier limit within the trailing 60s, the request returns 429 immediately — no queueing.
  • Counters expire automatically; no garbage collection is required.

The implementation lives in app/api/middleware/rate_limit.py. It's a single middleware so the same rate-limit logic applies to every REST endpoint without per-route config.

Why no fixed-window?

Fixed windows reset at clock boundaries (e.g. on the minute), which lets clients double the effective limit by bursting at the boundary. The sliding-window prevents that by always measuring the trailing 60s.

Spending cap (paid tier)

On the paid tier, you can set a monthly $-ceiling from the Billing dashboard. When the meter crosses the cap:

  • The server auto-pauses API calls for that account — every authenticated request returns 429 with details.tier: "paid" and details.reason: "spend_cap_reached".
  • We send a warning email at 80% of the cap so you can adjust before the pause kicks in.
  • Lift the pause by raising the cap or waiting for the next billing cycle.

This is opt-in. Without a cap set, paid-tier traffic runs uncapped and bills at the metered rate.

Demo widget (homepage)

The interactive demo at brandrna.com is not authenticated. It uses a 5-layer abuse defence:

  1. Cloudflare Turnstile bot challenge (per submission).
  2. Per-IP sliding-window quota (separate from authenticated limits).
  3. Per-IP daily call cap.
  4. Account-wide spend ceiling — cuts off all demo traffic at $10/day.
  5. Aggressive timeout on the scrape itself.

If you find yourself hitting the demo limits, sign up for a free key — you'll skip the abuse layer entirely and get 100 calls/month.

Practical patterns

  • Cache aggressively on your end too. Even at $0/cached-call, an HTTP round-trip costs ~80ms. See Cache brand packs in your own backend.
  • Batch where possible. If you're enriching a list of domains, fan out with limited concurrency (5–10 in flight). The burst limiter enforces this anyway, but explicit concurrency gives cleaner errors.
  • Watch metadata.cached. A false here means a fresh extraction was triggered — counts toward your quota even if the response was fast.
  • Backoff on 429. Wait at least 60s before retrying; the trailing window will have rolled off.

What's next

On this page