SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
10.5 KB · 234 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import { PLAN_LIMITS } from "@fetcha/core";3import { CodeBlock } from "@/components/ui/code-block";4import { DocPage } from "@/components/docs/doc-page";5import { A, Code, H2, H3, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose";6import { Callout } from "@/components/docs/callout";7import { Endpoint } from "@/components/docs/endpoint";8import { CodeTabs } from "@/components/docs/code-tabs";9import { ResponseExample } from "@/components/docs/response-example";10import { apiTabs } from "@/components/docs/snippets";1112export const metadata: Metadata = {13  title: "Rate Limits",14  description: "Concurrency, sliding-window rate limits, RATE_LIMITED vs CONCURRENCY_LIMIT, optional spending limits, USAGE_LIMIT_REACHED and response headers on the single unlimited plan.",15};1617const L = PLAN_LIMITS.unlimited;18const fmt = (n: number) => n.toLocaleString("en-US");1920export default function RateLimitsPage() {21  const perOrgMinute = Math.max(60, L.concurrency * 60);22  const perKeySecond = Math.max(10, L.concurrency * 2);23  return (24    <DocPage25      path="/docs/rate-limits"26      eyebrow="Reliability"27      title="Rate limits"28      description="Fetcha is a private platform with a single plan: there is no monthly request quota. Two operational limits remain — how many requests run at once and how fast you can submit them — plus spending limits you can set yourself."29      status="Stable"30    >31      <Callout variant="info" title="One plan, no meters">32        Every organization has the same limits, listed below and returned by <Code>GET /v1/usage</Code>. There is nothing to upgrade: hitting a limit means smoothing your traffic, not paying more.33      </Callout>3435      <H2>Concurrency</H2>36      <P>37        Concurrency is the number of fetch requests in flight at the same time, counted per <Strong>organization</Strong> (all projects and keys together). It is the limit that matters most in38        practice: a fetch takes a second or more, so sustained throughput is roughly concurrency divided by average latency.39      </P>40      <Table>41        <THead>42          <Tr>43            <Th>Limit</Th>44            <Th>Value</Th>45            <Th>Scope</Th>46          </Tr>47        </THead>48        <TBody>49          <Tr>50            <Td className="font-medium text-fg">Concurrent requests</Td>51            <Td mono>{fmt(L.concurrency)}</Td>52            <Td>Organization</Td>53          </Tr>54          <Tr>55            <Td className="font-medium text-fg">Concurrent browser renders</Td>56            <Td mono>{fmt(L.browser_concurrency)}</Td>57            <Td>Organization (subset of the above)</Td>58          </Tr>59          <Tr>60            <Td className="font-medium text-fg">Concurrent crawl jobs</Td>61            <Td mono>{fmt(L.crawl_concurrent_jobs)}</Td>62            <Td>Organization</Td>63          </Tr>64          <Tr>65            <Td className="font-medium text-fg">Monthly requests</Td>66            <Td mono>Unlimited</Td>67            <Td>&mdash;</Td>68          </Tr>69          <Tr>70            <Td className="font-medium text-fg">Log retention</Td>71            <Td mono>{L.retention_days} days</Td>72            <Td>Organization</Td>73          </Tr>74        </TBody>75      </Table>76      <P>77        When a request would exceed the limit it is refused immediately with <Code>429 CONCURRENCY_LIMIT</Code>; nothing is queued on Fetcha&apos;s side. A slot is released as soon as a request78        completes (successfully or not). As a safety net, a slot that is somehow never released expires after 150 seconds.79      </P>80      <ResponseExample status={429} statusText="Too Many Requests" body={{ error: { code: "CONCURRENCY_LIMIT", message: `Your organization allows ${L.concurrency} concurrent requests.`, request_id: "req_1l2m3n4o5p6q7r8s", details: { limit: L.concurrency } } }} />8182      <H2>Request rate</H2>83      <P>84        Independently of concurrency, submissions are metered with sliding windows. They are set well above what the concurrency limit lets you sustain, so they only bite on bursts or runaway85        loops. Three windows are checked on every fetch:86      </P>87      <Table>88        <THead>89          <Tr>90            <Th>Window</Th>91            <Th>Scope</Th>92            <Th>Rule</Th>93            <Th>Value</Th>94          </Tr>95        </THead>96        <TBody>97          <Tr>98            <Td>60 seconds</Td>99            <Td>Organization (sustained)</Td>100            <Td>101              <Code>concurrency × 60</Code> per minute102            </Td>103            <Td mono>{fmt(perOrgMinute)} / min</Td>104          </Tr>105          <Tr>106            <Td>1 second</Td>107            <Td>API key (burst)</Td>108            <Td>109              <Code>concurrency × 2</Code> per second110            </Td>111            <Td mono>{fmt(perKeySecond)} / s</Td>112          </Tr>113          <Tr>114            <Td>1 second</Td>115            <Td>Client IP (abuse)</Td>116            <Td>Fixed</Td>117            <Td mono>200 / s</Td>118          </Tr>119        </TBody>120      </Table>121      <P>122        Exceeding any window returns <Code>429 RATE_LIMITED</Code> with a <Code>Retry-After</Code> header (seconds, rounded up) and <Code>details.retry_after_ms</Code>.123      </P>124      <ResponseExample status={429} statusText="Too Many Requests" body={{ error: { code: "RATE_LIMITED", message: "Too many requests. Slow down and retry after the indicated delay.", request_id: "req_4e5f6g7h8i9j0k1l", details: { retry_after_ms: 640 } } }} />125126      <H3>RATE_LIMITED vs CONCURRENCY_LIMIT</H3>127      <Ul>128        <Li>129          <Code>RATE_LIMITED</Code>: you submitted too many requests in a short window. Wait <Code>Retry-After</Code> seconds, then continue. Smooth your submission rate.130        </Li>131        <Li>132          <Code>CONCURRENCY_LIMIT</Code>: too many requests are running right now. Do not sleep a fixed time; retry when one of your in-flight requests finishes. Use a semaphore or worker pool sized133          to {fmt(L.concurrency)} or less (see the <A href="/docs/examples#concurrency-within-plan-limits">concurrency example</A>).134        </Li>135      </Ul>136137      <H2>No monthly quota</H2>138      <P>139        There is no cap on the number of requests per month and no overage. Usage is still counted per organization and per project (<Strong>every request that reaches the execution pipeline140        counts</Strong>, including those that end blocked or failed) so that the dashboard, <Code>GET /v1/usage</Code> and your optional spending limits have accurate figures. Requests refused141        earlier (authentication, validation, rate limits, session errors) are not counted.142      </P>143144      <H2>Spending and project limits</H2>145      <P>146        You can cap usage yourself in the dashboard. These are guard-rails against runaway jobs, not billing: spend is an internal cost estimate and nothing is invoiced. All of them produce the same{" "}147        <Code>USAGE_LIMIT_REACHED</Code> code with a specific message and details:148      </P>149      <Table dense>150        <THead>151          <Tr>152            <Th>Limit</Th>153            <Th>Scope</Th>154            <Th>Effect when reached</Th>155            <Th>details</Th>156          </Tr>157        </THead>158        <TBody>159          <Tr>160            <Td>Monthly request limit</Td>161            <Td>Project</Td>162            <Td>Fetches in that project are refused</Td>163            <Td mono>{`{ limit, used }`}</Td>164          </Tr>165          <Tr>166            <Td>Hard spending limit (USD estimate)</Td>167            <Td>Organization</Td>168            <Td>All fetches are refused</Td>169            <Td mono>{`{ limit_usd, spent_usd }`}</Td>170          </Tr>171          <Tr>172            <Td>Hard spending limit (USD estimate)</Td>173            <Td>Project</Td>174            <Td>Fetches in that project are refused</Td>175            <Td mono>{`{ limit_usd, spent_usd }`}</Td>176          </Tr>177          <Tr>178            <Td>Soft spending limit (USD estimate)</Td>179            <Td>Organization or project</Td>180            <Td>Alert only; traffic continues</Td>181            <Td>&mdash;</Td>182          </Tr>183        </TBody>184      </Table>185      <ResponseExample186        status={402}187        statusText="Payment Required"188        title="402 · project request limit reached (set by you)"189        body={{ error: { code: "USAGE_LIMIT_REACHED", message: "Monthly request limit of 50,000 reached for this project.", request_id: "req_8s9t0u1v2w3x4y5z", details: { limit: 50000, used: 50000 } } }}190      />191      <Callout variant="info" title="Spend is an estimate">192        Spend is Fetcha&apos;s internal cost estimate of your usage, accumulated over the calendar month (UTC). It is what <Code>GET /v1/usage</Code> reports as <Code>spend_usd</Code>. Usage counters193        are cached for up to 20 seconds, so enforcement of a limit you set can lag a few requests behind.194      </Callout>195196      <H2>Checking your usage</H2>197      <Endpoint method="GET" path="/v1/usage" scope="usage:read" status="Live" />198      <CodeTabs tabs={apiTabs({ method: "GET", path: "/v1/usage" }, ["curl", "javascript", "python"])} />199      <ResponseExample200        status={200}201        body={{202          period_start: "2026-09-01T00:00:00.000Z",203          plan: { id: "unlimited", label: "Unlimited", monthly_requests: null, concurrency: L.concurrency },204          organization: { requests: 12840, spend_usd: 3.2115 },205          project: { requests: 9911, spend_usd: 2.4402, successful_requests: 9640, success_rate: 97.3, bandwidth_bytes: 3892214411, latency_p50_ms: 1180, latency_p95_ms: 4210 },206          remaining_requests: null,207        }}208      />209      <Ul>210        <Li>211          <Code>organization</Code> covers every project; <Code>project</Code> is the key&apos;s project only.212        </Li>213        <Li>214          <Code>plan.monthly_requests</Code> and <Code>remaining_requests</Code> are <Code>null</Code>: there is no quota. A project-level request limit you configured is reported in the215          dashboard, not here.216        </Li>217        <Li>218          <Code>success_rate</Code> is a percentage with one decimal, or <Code>null</Code> when the project made no requests this month.219        </Li>220      </Ul>221222      <H2>Response headers</H2>223      <P>Fetcha currently sets these headers on every API response. There are no <Code>X-RateLimit-*</Code> counters; use <Code>GET /v1/usage</Code> for month-to-date figures.</P>224      <CodeBlock225        lang="text"226        code={`X-Fetcha-Request-ID: req_…     # always; quote it to support227X-Fetcha-Version: 0.2.0         # API version228Cache-Control: no-store         # responses are never cacheable229Retry-After: <seconds>          # only on 429 RATE_LIMITED`}230      />231    </DocPage>232  );233}234