Rate limits
Concepts

Rate limits

Fixed per-minute and per-day windows per key, by tier — read the X-RateLimit-* headers rather than hardcoding.

View as MarkdownUpdated Aug 19, 2026

Tiers#

TierPer minutePer dayWho
Personal605,000Personal keys. The budget is shared across all personal keys of the same creator — minting more keys does not multiply it.
App30050,000Keys bound to an approved app, per key. Higher per-app overrides on request.
First partyunlimitedunlimitedDropfans-operated integrations are exempt.

Windows are fixed, not rolling: the minute window is the current clock minute, the day window is the current UTC day. Both reset on the boundary.

Tip

Always read X-RateLimit-Limit off the response instead of assuming a number — env-level and per-app overrides mean the live limit can differ from the defaults printed here.

Headers on every response#

HeaderMeaning
X-RateLimit-Tierpersonal, app or first_party.
X-RateLimit-LimitRequests allowed in the current minute window.
X-RateLimit-RemainingRequests left in the current minute window.
X-RateLimit-ResetUnix seconds when the minute window resets.
X-RateLimit-Limit-DayRequests allowed in the current UTC-day window.
X-RateLimit-Remaining-DayRequests left in the current UTC-day window.
X-RateLimit-Reset-DayUnix seconds when the day window resets.
Retry-AfterOn 429 only — seconds to wait before retrying.

First-party responses carry only the tier header; limited tiers carry the full set.

When you hit the limit#

{ "error": "Rate limit exceeded", "code": "rate_limited" }

Status 429, with Retry-After in seconds. Wait that long, then resume — do not hammer the boundary.

The separate posting cap#

Creating feed posts has its own cap, independent of the request budget: 5 posts per creator per rolling 24 hours — the same cap the dashboard composer enforces. Exceeding it also returns 429 with Retry-After, but the body is prose without a code:

{ "error": "You’ve reached the posting limit (5 per day). Try again in 3 hours." }

Tell the two apart by the code field: rate_limited means request volume; no code on a posts 429 means the daily posting cap.

Retry wrapper#

async function callApi(url, options = {}, attempt = 0) {
  const res = await fetch(url, {
    ...options,
    headers: {
      Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY,
      ...(options.headers || {}),
    },
  });
  if (res.status === 429 && attempt < 3) {
    const wait = parseInt(res.headers.get('Retry-After') || '60', 10);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return callApi(url, options, attempt + 1);
  }
  return res;
}

Capacity planning#

Most throttling comes from polling too much, not from real traffic. Budget it: polling check-status once a minute costs 1,440 requests a day — well inside the app tier, but over a quarter of a personal day budget. Use the batch endpoints (one call covers up to 200 drops), follow the polling patterns, and watch that page for webhooks, which will replace most polling.

Next: Errors or Webhooks & polling.

Questions? [email protected]