# Rate Limits (/docs/rate-limits) Rate Limits [#rate-limits] SocialCrawl keeps throttling simple. Volume is gated by **credits**, with two hard request controls on every authenticated `/v1` key: a **requests-per-minute** window and a **concurrency** cap. Both 429s are unbilled. The limits [#the-limits] | Limit | Applies to | Value | What happens when you hit it | | --------------------------- | -------------------------------- | ---------------------------- | -------------------------------------------------------------------------------- | | **Request rate** | Authenticated API, per API key | **600 requests / minute** | `429 RATE_LIMITED` with `Retry-After` and `X-RateLimit-*` headers | | **Concurrency** | Authenticated API, per API key | **50 simultaneous requests** | `429 CONCURRENCY_LIMIT` with a short `Retry-After` header | | **Credits** | Authenticated API, per account | Your remaining balance | `402 INSUFFICIENT_CREDITS` - the de-facto volume limit | | **Per-key credit limit** | Authenticated API, per API key | Optional, you set it | `402 KEY_BUDGET_EXCEEDED` - only that key stops; the account is unaffected | | **Anonymous Explorer gate** | Unauthenticated `/explore` embed | **5 calls / UTC day / IP** | `429 Daily limit reached` | | **API keys** | Per account | **5 active keys** | Key creation fails; revoke one first. See [Authentication](/docs/authentication.md) | 1\. Request rate: 600 / minute per key [#1-request-rate-600--minute-per-key] Every authenticated request counts toward a **sliding 1-minute window of 600 requests per API key**, including cache hits (they are free of credit cost but still load Redis/Postgres). The default is intentionally generous so legitimate traffic never notices it; it exists to stop runaway clients, not to shape normal product usage. ```json { "success": false, "error": { "type": "RATE_LIMITED", "message": "Request rate limit exceeded. Limit: 600 requests per minute. Honor the Retry-After header, then back off with jitter (see /docs/rate-limits).", "status": 429, "doc_url": "https://www.socialcrawl.dev/docs/errors#rate-limited" }, "credits_remaining": null, "request_id": "req-abc123" } ``` A `429 RATE_LIMITED` **never costs credits** - the limiter runs after auth and before credit deduction. 2\. Concurrency: 50 in-flight requests per key [#2-concurrency-50-in-flight-requests-per-key] Separately, a key may have at most **50 requests in flight at once**. This is a concurrency limit, not a time window: a slot frees the instant one of your in-flight requests returns. Send the 51st request while 50 are still open and it is rejected: ```json { "success": false, "error": { "type": "CONCURRENCY_LIMIT", "message": "Too many concurrent requests. Limit: 50. Honor the Retry-After header, then back off exponentially with jitter (see /docs/rate-limits).", "status": 429, "doc_url": "https://www.socialcrawl.dev/docs/errors#concurrency-limit" }, "credits_remaining": null, "request_id": "req-abc123" } ``` A concurrency `429` **never costs you credits** - the limiter rejects the request before any billing step runs, so nothing is deducted in the first place. (`credits_remaining` is `null` on this response because the balance is never read.) 3\. Credits: the real volume limit [#3-credits-the-real-volume-limit] There is no daily or monthly request cap beyond the 600/min window. Your **credit balance is the volume limit for sustained work.** Flat endpoint costs run from 0 to 50 credits per call, and a set of composite endpoints is metered — they bill a per-request figure inside a published range that scales with how far the call fans out. Price every endpoint your job touches from [Endpoint Pricing](/docs/endpoint-pricing.md) before you size the job; see [Credits](/docs/credits.md) for how charging and refunds work. Size a batch job by credits, not by an artificial request quota: 20,000 credits is 20,000 standard calls, run them as fast as the 50-concurrency cap and 600/min window allow. 4\. Per-key credit limits: an optional cap you set [#4-per-key-credit-limits-an-optional-cap-you-set] The balance above is shared by every key on the account, so by default any one key can spend all of it. You can cap an individual key with a **credit limit**, after which that key returns `402 KEY_BUDGET_EXCEEDED` and stops spending — while your other keys carry on untouched. This is the control to reach for when a test, CI, or staging key shares an account with production traffic. It is off by default; a key with no limit behaves exactly as it always has. Set one in **Dashboard → API Keys**, and see [Authentication](/docs/authentication.md#per-key-credit-limits) for how the counter and resets work. 5\. Anonymous Explorer gate: 5 / day / IP [#5-anonymous-explorer-gate-5--day--ip] The unauthenticated Explorer embed on the marketing site is capped at **5 calls per UTC day per IP**. This gate exists only on the anonymous proxy - it does **not** apply to authenticated API keys. Sign up for a free key to remove it; you get a credit balance and only the per-key rate and concurrency limits apply. Rate-limit headers [#rate-limit-headers] Every response from the authenticated API carries live headroom headers so you (or an agent) can self-throttle **before** hitting the wall: | Header | Meaning | | ------------------------- | ------------------------------------------------------------------------ | | `X-RateLimit-Limit` | Request-window cap (`600` per minute). | | `X-RateLimit-Remaining` | Requests still available in the current sliding window. | | `X-RateLimit-Reset` | Unix epoch seconds when the window resets (omitted if limiter degraded). | | `X-Concurrency-Limit` | The concurrency cap (`50`). | | `X-Concurrency-Remaining` | Concurrency slots still free as this request entered. | | `Retry-After` | **On a `429` only.** Seconds to wait before retrying. | These two families are **not** aliases of each other. `X-RateLimit-*` is the requests-per-window limiter; `X-Concurrency-*` is the in-flight cap. Generic clients that only understand `X-RateLimit-*` now read the real rate limit. On `RATE_LIMITED`, `Retry-After` is the seconds until the window resets. On `CONCURRENCY_LIMIT`, `Retry-After` is a short static hint (`1` second) because a slot can free the moment any in-flight request returns. If the limiter's backing store is briefly unavailable we fail **open**: nothing is throttled, and `X-RateLimit-Remaining` reports the nominal cap rather than a measured figure. A missing `X-RateLimit-Reset` is the signal to distrust the other two on that response. The headroom headers are also absent on `401` responses, which are rejected before the limiter runs. Retry guidance [#retry-guidance] When you get a `429`: 1. **Read `error.type`.** `RATE_LIMITED` means slow down your request *rate*; `CONCURRENCY_LIMIT` means narrow your worker pool. 2. **Honor `Retry-After`.** Wait at least that many seconds before the first retry. 3. **Then back off exponentially with jitter.** If a retry also `429`s, double the wait and add random jitter so a burst of your own workers does not resynchronize and stampede the cap together. 4. **Cap the retries.** Give up after a handful of attempts and surface the error. A drop-in TypeScript helper: ```ts async function callWithBackoff( url: string, init: RequestInit, maxRetries = 5, ): Promise { for (let attempt = 0; ; attempt++) { const res = await fetch(url, init); // Only 429s are retryable here. 402 (out of credits) and other // 4xx client errors are not - retrying them just wastes time. if (res.status !== 429 || attempt >= maxRetries) return res; const retryAfter = Number(res.headers.get("Retry-After")) || 1; // Exponential backoff with full jitter, seeded by Retry-After. const backoff = retryAfter * 2 ** attempt; const jittered = backoff * (0.5 + Math.random() * 0.5); await new Promise((r) => setTimeout(r, jittered * 1000)); } } ``` Sizing a batch job with confidence [#sizing-a-batch-job-with-confidence] To drain a large account safely: keep your worker pool at or under **50 concurrent requests**, stay under **600 requests per minute**, watch `X-Concurrency-Remaining` and `X-RateLimit-Remaining` to stay off the wall, budget credits per page, and loop your paginated endpoints until `has_more` is `false`. See the [Recipes](/docs/recipes.md) for a full "drain an account" loop. Which errors are safe to retry - and which are not - is covered on the [Error Handling](/docs/errors.md) page, which also ships copy-paste [backoff-with-jitter loops](/docs/errors.md#handling-retries) in cURL, Python, and Node that honor `Retry-After`.