Skills に戻る
samber/developer-platform-skillsチェック済み

SKILL DETAIL

api-rate-limit-policy

samber/developer-platform-skills/api-rate-limit-policy

Design the rate-limit policy a public API publishes to its consumers - the metering model per paradigm (REST request counting vs GraphQL cost points), tier and quota-vs-burst numbers, multi-tenant fairness, rate-limit headers (legacy X-RateLimit-* vs IETF RateLimit fields), 429 and Retry-After behavior, enterprise and partner overrides, and change-notice rules. Use whenever the user mentions rate limits, quotas, throttling tiers, 429 responses, noisy neighbors, or limit-increase requests - even if they never say "rate-limit policy". Policy layer only, not gateway configuration. Do NOT use for the 429 error envelope - use samber/developer-platform-skills@api-error-design instead.

インストール · 327出典を見る

Installation

npx skills add https://github.com/samber/developer-platform-skills --skill api-rate-limit-policy

スキルファイル

SKILL.md

最終同期 · 2026/10/23

evals/evals.json›
{
  "skill_name": "api-rate-limit-policy",
  "evals": [
    {
      "id": 1,
      "prompt": "We're Loamly, a B2B inventory-management SaaS, opening our public API next quarter: a set of REST endpoints plus a GraphQL endpoint for partner integrations. I want to keep the docs simple with one number: 300 requests per minute per API key, covering both surfaces. Plans are Free, Growth at $49/mo, and Scale at $299/mo, and I figured each plan just gets a bigger multiplier of the 300. Draft our published rate-limit policy.",
      "expected_output": "A published rate-limit policy that meters REST and GraphQL separately (request counting vs cost points), publishes burst and sustained as two numbers per tier, keys each tier deliberately, states limit scope, and labels numbers as hints not SLA - preceded by batched clarifying questions.",
      "files": [],
      "expectations": [
        "Asks batched clarifying questions before publishing numbers, covering at least abuse pressure, region count, gateway/edge capability, and deadline or effort ceiling",
        "Rejects a single 300 req/min limit shared across REST and GraphQL: REST is metered by request counting and GraphQL by cost points, as two separately published models",
        "Expresses the GraphQL limit as a budget of cost points per window with a published cost formula, not a request count",
        "States explicitly whether the REST and GraphQL limits share anything (such as a common concurrency cap) instead of leaving the relationship between the two surfaces unstated",
        "Publishes burst and sustained as two explicit numbers per tier, never one blended figure",
        "Recommends token bucket as the default algorithm, mapping bucket depth to the burst number and refill rate to the sustained quota",
        "Frames limits as identity/auth-scope x endpoint-class x account-override, treating the plan-to-limit mapping as a deliberate choice rather than the automatic default",
        "States which identifier each tier is keyed on and changes it up the ladder (IP for anonymous callers, API key or org for paid tiers)",
        "Carves expensive operations out of the general budget with per-endpoint-class limits instead of one account-wide number",
        "States the geographic scope of the published limit (global vs per-region enforcement)",
        "Labels the published numbers operational hints rather than an SLA, reserving in writing the right to lower limits for stability",
        "Structures the tier ladder so anonymous/free callers learn limits from headers, self-serve paid tiers get a documented exact-numbers table, and any enterprise tier gets a process rather than a public number"
      ]
    },
    {
      "id": 2,
      "prompt": "At Ferroline we sell shipping-rate APIs to logistics companies. Our public docs describe our limiter as a token bucket: incoming requests per API key go into a queue and we drain exactly 25 per second at a steady pace no matter how many pile up. An engineer on my team wants to replace it with a fixed one-minute window of 1,500 requests because the gateway code would be much simpler. Review the policy wording and tell us whether to make the switch.",
      "expected_output": "A review that identifies the described mechanism as leaky bucket despite the token-bucket name, rewrites the policy to describe behavior rather than rely on the algorithm name, and declines the fixed-window switch for a public developer API, citing its boundary and stampede flaws.",
      "files": [],
      "expectations": [
        "Identifies the described mechanism as leaky-bucket behavior (constant drain rate, no banked burst capacity), not a token bucket, despite what the docs call it",
        "Flags token bucket vs leaky bucket as a commonly inverted naming pair and requires the published policy to describe the behavior (whether idle time banks burst capacity) rather than rely on the algorithm name",
        "Treats the mismatch between the documented algorithm name and the enforced behavior as an audit finding in its own right",
        "States fixed window's boundary flaw: up to 2x the published limit can pass at the window boundary",
        "States fixed window's stampede flaw: throttled clients synchronize their retries at the reset boundary",
        "Declines the fixed-window switch for this public developer API, confining fixed window to internal or low-stakes limits where the boundary flaw is harmless",
        "Cites a named platform precedent that accepts fixed window's flaw purely for simplicity in a low-stakes context (Sentry), and distinguishes that context from Ferroline's",
        "Asks or determines whether a burst is a threat or a feature for Ferroline's downstream before choosing between keeping leaky bucket and moving to token bucket",
        "Notes that token bucket and leaky bucket are an effort tie (both maintain one bucket per key, differing in intent, not implementation weight), so the simplicity argument does not favor fixed window over them",
        "Frames the algorithm as a policy choice defining what the published number promises (burst behavior, boundary behavior), not an internal gateway detail",
        "Conditions any promotion of the sliding-window option on adversarial or scraping pressure, naming its higher compute cost",
        "Requires burst and sustained to be published as two explicit numbers under whichever algorithm is kept"
      ]
    },
    {
      "id": 3,
      "prompt": "Kolstrom Analytics runs a multi-tenant data API. Our biggest tenant kicks off a batch export at 2am that pushes every other tenant's p99 latency way up, but they never exceed their 10,000 req/min cap, so we can't point at a violation. Support wants to cut everyone's cap to 4,000 req/min to make it stop. Accounts have up to 40 seats sharing one org-level API key. Design fairness into our limits without hard-blocking the big customer.",
      "expected_output": "A layered fairness design (tenant ceiling, per-user limits inside the tenant allocation, per-endpoint-class carve-out for exports, concurrency limits) with weighted sharing instead of a hard cliff, its trade named, load shedding disclosed as a distinct mechanism, and internal machinery kept out of the published policy.",
      "files": [],
      "expectations": [
        "Rejects cutting every tenant's cap to 4,000 req/min as the fix",
        "Identifies the incident as the canonical noisy-neighbor case where the caller stays under its own cap, so tighter per-tenant request caps alone cannot fix it",
        "Applies a tenant-level ceiling as the first fairness layer, where the tier table applies",
        "Applies per-user limits inside the tenant's allocation as the second layer, addressing the 40-seat shared org key so one seat cannot spend the whole tenant budget",
        "Applies a per-endpoint-class carve-out as the third layer, isolating the expensive export operations from the general budget",
        "Proposes weighted sharing (per-tenant weights with capacity divided proportionally) instead of a hard per-tenant cliff, so a small tenant is never starved to zero",
        "Names the weighted-sharing trade: a deterministic guaranteed rate is given up to eliminate the fully-blocked-small-tenant cliff",
        "Adds a concurrency limiter for the expensive endpoints, not only a requests-per-window number",
        "Presents defense-in-depth with multiple simultaneous limiter types as the mature end state rather than picking one mechanism",
        "Distinguishes load shedding from rate limiting (it keys off overall system state, so a well-behaved tenant can still be shed) and requires the published policy to disclose this rather than overclaim fairness",
        "Keeps internal fairness machinery (weights, deficit round robin, shedders) out of the published policy, documenting only externally observable effects",
        "Acknowledges the quota paradox knowingly: throttling reduces the throttled tenant's own availability even when spare capacity existed, and this is an accepted trade, not a hidden one"
      ]
    },
    {
      "id": 4,
      "prompt": "Nuvexa is a payments API. Our platform team wants to modernize our rate-limit headers: drop the X-RateLimit-Limit/Remaining/Reset trio and emit only the standards-track IETF RateLimit and RateLimit-Policy fields, since the legacy headers are cruft. Our CDN edge can emit structured fields basically for free. About 60% of our traffic comes through our own official SDKs. Settle our header strategy.",
      "expected_output": "A header decision that refuses IETF-fields-alone as off the menu, keeps the legacy trio (alone or in the both-families option promoted by the cheap edge emission), names the re-promotion trigger for IETF-only, documents reset semantics, and treats all headers as advisory hints.",
      "files": [],
      "expectations": [
        "Refuses to ship the IETF fields as the only limit-state signal, treating IETF-alone as removed from the menu rather than merely ranked last",
        "States the core reason: emitting only IETF fields breaks the existing clients and SDKs that parse the legacy X-RateLimit-* trio",
        "Cites thin IETF adoption (GitHub and Stripe emit no IETF fields) and that the spec is a pre-RFC Internet-Draft",
        "Notes the draft has already changed field syntax across draft versions, so today's field names can move",
        "Keeps the legacy X-RateLimit-* trio in whatever gets emitted, either alone or alongside the IETF fields in the both-families option",
        "Uses the CDN's cheap structured-field emission as the stated promoter toward the both-families option",
        "Names a re-promotion trigger for IETF-fields-alone: the draft reaching RFC status, or a second Tier-1 platform adopting it",
        "Cites RateLimit-Policy's distinct value: it lets a well-behaved client self-throttle before its first 429, which the legacy trio cannot express",
        "Cites named Tier-1 production adoption of both header families (Cloudflare) as traction evidence for the both-families option",
        "Requires documenting the Reset semantics explicitly (epoch timestamp vs seconds-remaining), since vendors genuinely differ",
        "States that headers are advisory hints, never guarantees: a remaining count does not promise that many successful calls",
        "Weighs the 60% own-SDK traffic share when ranking: an audience you control at both ends weakens the legacy trio's compatibility argument, since the fleet can move in one SDK release"
      ]
    },
    {
      "id": 5,
      "prompt": "Audit and redesign how Brightmoor's API responds when it throttles. Today: REST returns 403 with {\"error\": \"slow down\"} and no timing information; our CDN caches those responses for 60 seconds; GraphQL returns HTTP 200 with a THROTTLED code in the errors array, which our docs never mention. We enforce three different limits (per-key rate, per-org concurrency, and a search-endpoint cap) and integrators complain they can't tell which one they tripped. We planned to invent a dedicated rate-limit JSON response format to fix that. Also draft the retry guidance for our docs.",
      "expected_output": "A throttle-response redesign: 429 with delay-seconds Retry-After on every REST throttle, CDN caching removed, the 200-THROTTLED path documented loudly and machine-readable, a which-limit-was-hit signal, the standard error envelope reused instead of a new format, and retry guidance covering Retry-After, jittered backoff, and the hammering consequence.",
      "files": [],
      "expectations": [
        "Replaces the 403 with HTTP 429 plus a Retry-After header on every REST throttle, no exceptions",
        "Specifies Retry-After as delay-seconds rather than HTTP-date, citing the client bug where an HTTP-date is parseInt-ed into NaN and triggers a retry storm",
        "Removes the 60-second CDN caching of throttle responses: RFC 6585 forbids caching a 429, and caching stretches the apparent throttle beyond its real duration",
        "Rejects inventing a dedicated rate-limit response format: the 429 body is a plain instance of the API's standard error envelope with a machine-readable code, whose shape is owned by the error-design discipline",
        "Adds a machine-readable signal disambiguating which of the three limits was hit, modeled on Stripe-Rate-Limited-Reason",
        "Treats the undocumented 200-THROTTLED GraphQL path as a named design failure, because a status-code-only client treats it as success",
        "Where GraphQL keeps a body-level throttle signal, requires it documented as loudly as a status code and kept machine-readable, with the REST 429 case and the GraphQL 200 case documented separately so clients learn both, never mixed silently",
        "Retry guidance instructs clients to honor Retry-After",
        "Retry guidance requires exponential backoff with jitter, with jitter stated explicitly",
        "Retry guidance states the consequence for continuing to hammer while throttled, such as banning the integration",
        "States the precedence rule: Retry-After takes precedence over any reset time computed from limit-state headers",
        "Applies the machine-readability gate: 100% of throttle responses must carry a machine-readable signal a documented client can branch on, and the undocumented 200 path currently fails it",
        "Publishes retry expectations only, leaving client-side retry mechanics (backoff implementation, retry budgets, idempotency keys) to the idempotency-and-retry discipline"
      ]
    },
    {
      "id": 6,
      "prompt": "Vantiq Cloud is a B2B data platform. Two enterprise prospects each demanded roughly 10x our published Pro limit during contract negotiations. Sales wants to add a public 'Enterprise: 500,000 requests/day' row to the pricing page, with increases granted case-by-case at the account executive's discretion and attached to the customer admin's user account. One prospect also assumed that buying the Enterprise plan automatically raises their limits. Set up our limit-increase process.",
      "expected_output": "An override path where the enterprise tier publishes a documented process instead of a public number, overrides are keyed to the authenticated principal with structured requests and lead times, the plan-implies-limits question is answered explicitly in docs, one dimension is non-negotiable, and the right to lower overrides is reserved in writing.",
      "files": [],
      "expectations": [
        "Rejects publishing a fixed public Enterprise number: self-serve tiers publish exact numbers, the top tier publishes a process",
        "Keys overrides to the authenticated principal (API key, org, or app installation), never to a person, rejecting attachment to the admin's user account",
        "Replaces ad-hoc account-executive discretion with a named, documented override mechanism, since a contact-sales path with no process is the gap",
        "Presents the named override shapes (ticket-based with lead time, spend-based auto-promotion, plan-gated, time-boxed self-serve burst) and picks one deliberately",
        "Specifies a lead time for increase requests, citing vendor precedent such as at least 6 weeks for large increases or 24-hour-minimum structured tickets",
        "Requires the increase request to carry structured fields such as the key or org, requested rate, and dates or duration",
        "States explicitly in the docs whether a higher-priced plan implies higher limits, citing the precedent where an Enterprise plan does not raise limits until they are explicitly activated",
        "Names at least one limit dimension that no override touches, and says so in the policy",
        "Reserves in writing the right to lower any granted override for platform stability",
        "Requires every granted override to be recorded and keyed somewhere auditable, since an override granted verbally, keyed to nothing, documented nowhere is a named failure mode",
        "Routes overrides through support, sales, or an account manager rather than a dashboard toggle, noting the time-boxed self-serve burst as the rare exception",
        "Flags persistent override-request volume per quarter as the signal that the published tiers are set too low"
      ]
    },
    {
      "id": 7,
      "prompt": "Ostrafin runs a fintech API. Abuse is eating our capacity, so we must cut the free-tier limit from 1,000 to 300 requests/hour. Our platform lead insists rate limits are part of the API contract, so this has to ship as API v2 with our standard 18-month deprecation window and Sunset headers. A colleague says the opposite: limits aren't breaking, just flip the number tonight. While we're in there, we also want to switch X-RateLimit-Reset from epoch seconds to seconds-remaining. Write our change plan.",
      "expected_output": "A change plan that treats the limit cut as non-breaking (no v2, no Sunset machinery) yet refuses the silent overnight flip: written notice scaled to blast radius with a lead-time floor, an observe-baseline-refine rollout with caller communication, the reset-semantics switch escalated to a major standalone announcement, and emergency reductions reserved in writing.",
      "files": [],
      "expectations": [
        "States that a rate-limit change is treated industry-wide as non-breaking and requires no version bump, rejecting the v2-with-Sunset-headers plan",
        "Also rejects flipping the number overnight: precisely because no deprecation machinery fires on a limit change, a written notice policy must carry the communication",
        "Keeps version-deprecation machinery (multi-month windows, Deprecation and Sunset headers) as a separate regime owned by the versioning policy, not applied to limit changes",
        "Scales notice to blast radius and applies a lead-time floor to the decrease, citing the 6-week vendor precedent as a symmetric floor",
        "Rolls the tightening out via the observe (log without enforcement), baseline (enforce at a high ceiling), refine (tighten from observed data) sequence on live traffic",
        "Requires communicating with affected callers before each tightening step",
        "Flags the reset-semantics switch from epoch seconds to seconds-remaining as a change that breaks client parsing invisibly and warrants a major announcement by itself, never shipped silently alongside the limit cut",
        "Reserves emergency reductions for platform stability in writing, so they are policy rather than surprise",
        "Counters the part-of-the-contract framing: published limits are operational hints, not an SLA, and the policy says so",
        "Considers a secondary abuse guard triggered by request shape as a complement to cutting every free caller's quota, with its existence and trigger shape documented",
        "Uses in-band header exposure so well-behaved clients adapt automatically, alongside the out-of-band notice",
        "Produces a standing written change-notice policy that explains the reasons for changes, not just a one-off migration email"
      ]
    },
    {
      "id": 8,
      "prompt": "Tessellate is a B2B ecommerce platform designing rate limits for our new public GraphQL Admin API. The plan: measure each query's cost after it executes based on response size and deduct it from the account's points budget; Premium merchants at $800/mo get both a bigger budget and permission to run bigger single queries, so the max cost per query scales with plan; mutations cost the same as queries since they're all just requests. What numbers and rules should we publish?",
      "expected_output": "A cost-metering policy with pre-charge on a static estimate plus refund against actual cost, a published per-field formula integrators can compute pre-flight, a plan-independent single-query cap, mutations weighted above reads, per-response cost and budget exposure, and structural schema guards deferred to GraphQL schema design.",
      "files": [],
      "expectations": [
        "Rejects post-execution-only costing: admission control requires a static worst-case cost estimate charged before execution, with the difference against actual cost refunded afterward",
        "Prices cost from the query's shape and fields via a published formula an integrator can compute before sending, not from response size",
        "Exposes both requested and actual cost on every response (such as an extensions cost object) so clients can see the pre-charge and refund accounting",
        "Pins the single-query maximum cost independently of plan and states it explicitly, rejecting a plan-scaled max query cost",
        "Names the portability trap motivating the plan-independent cap: a query written against a bigger plan's allowance instantly fails on a smaller-plan account",
        "Weights mutations above reads of equivalent shape (general guidance 2-5x, with a named vendor precedent at 10x), rejecting equal costing",
        "Publishes per-field weights, not just the budget, for example scalar 0, object 1, and connections sized by their first/last pagination arguments",
        "Exposes the remaining budget and the restore rate on every response so a client can model the bucket locally",
        "Expresses the budget as bucket depth plus restore rate, burst and sustained as two numbers, not one blended figure",
        "Recommends a debug affordance returning a per-field cost breakdown so integrators can understand why a query costs what it costs",
        "Recommends a budget-introspection mechanism that costs nothing to query, so a client can check its remaining budget for free",
        "Keeps structural query guards (depth ceilings, required pagination arguments, persisted queries) out of scope as schema design owned by the GraphQL API design discipline, while owning the pricing and budget policy"
      ]
    },
    {
      "id": 9,
      "prompt": "Audit Corvana's rate-limit documentation against reality. The docs page says 'Global limit: 1,200 requests/min per API key' and our enterprise MSA cites that number as an SLA-backed guarantee. The gateway actually enforces 1,200/min independently in each of our four regions (us-east, eu-west, ap-south, sa-east). There's also an unlisted anti-abuse rule that blocks any key opening more than 50 concurrent connections. Sandbox keys get 12,000/min so developers can load-test freely. Docs mention none of this. Limit-related support tickets doubled last quarter. What do we fix?",
      "expected_output": "An audit that quantifies the per-region-as-global misstatement (real throughput 4,800/min), surfaces the undocumented concurrency guard with its unit named, reconciles the SLA claim against hints-not-SLA policy, challenges the 10x sandbox limiter, applies both measurement gates, and routes any resulting tightening through change-notice rules with baselined metrics.",
      "files": [],
      "expectations": [
        "Flags the per-region enforcement presented as global: a caller spreading traffic across the four regions gets 4,800 req/min, four times the published number, so the published limit must state its scope",
        "Treats the region multiplication as a real exploit vector, not a documentation nit",
        "Flags documented-vs-enforced drift as an audit finding in either direction and requires diffing the docs against the enforcing configuration both ways",
        "Flags the undocumented concurrency guard: a secondary limit discoverable only by tripping it is a named failure, and its existence and trigger shape (more than 50 concurrent connections) must appear in the docs",
        "Names the concurrency guard's metering unit explicitly as in-flight requests rather than requests per window, a fundamentally different unit callers must plan for",
        "Requires the concurrency guard's throttle signal to be machine-readable and documented, since the machine-readability gate covers every limiter's response",
        "Flags the MSA language: a published limit cited in a contract as an SLA commitment without the policy saying it is one is a named failure mode, and published numbers are operational hints",
        "Challenges the 12,000/min sandbox tier: the default is reusing production numbers in test mode so integrators' throttling code stays honest",
        "Where a separate pre-production quota is kept, shapes it as an aggregate volume-and-scope cap against evaluation abuse, not a faster per-minute limiter",
        "Applies the documentation-enforcement parity gate: zero drift, with every documented number enforced and every enforced limit, secondary guards included, at least acknowledged in the docs",
        "Treats any resulting tightening, such as enforcing 1,200 truly globally, as a limit change requiring notice scaled to blast radius or an observe-baseline-refine rollout, not a silent configuration fix",
        "Ties the doubled support tickets to a measurement plan baselined from Corvana's own first month, covering per-tier 429 rate, the share of 429s retried inside the Retry-After window, limit-related tickets, and override-request volume, since no industry thresholds exist"
      ]
    }
  ],
  "trigger_queries": [
    { "query": "Design rate-limit tiers for our public REST API - we have free, pro, and enterprise plans", "should_trigger": true },
    { "query": "How should rate limiting work across our REST API and the new GraphQL endpoint?", "should_trigger": true },
    { "query": "Our biggest customer's nightly batch jobs slow everyone else down but they never exceed their cap - what do we do?", "should_trigger": true },
    { "query": "What numbers should we publish on our API limits page?", "should_trigger": true },
    { "query": "Should we use token bucket or sliding window for our public API?", "should_trigger": true },
    { "query": "Customers keep asking for rate limit increases - how do we handle enterprise limit requests?", "should_trigger": true },
    { "query": "What rate limit headers should our API return?", "should_trigger": true },
    { "query": "X-RateLimit headers vs the new IETF RateLimit fields - which should we emit?", "should_trigger": true },
    { "query": "How much notice do we owe integrators before lowering API quotas?", "should_trigger": true },
    { "query": "Design a cost-point budget for our GraphQL API", "should_trigger": true },
    { "query": "How do we price GraphQL queries for rate limiting purposes?", "should_trigger": true },
    { "query": "What signals should our 429 response carry so clients know what to do?", "should_trigger": true },
    { "query": "Should burst limits be separate from sustained limits?", "should_trigger": true },
    { "query": "One tenant is hogging our API - design fairness without hard-blocking them", "should_trigger": true },
    { "query": "quota vs burst - how many requests per minute should each plan get?", "should_trigger": true },
    { "query": "Audit our rate limit docs against what the gateway actually enforces", "should_trigger": true },
    { "query": "We keep getting complaints that clients hit throttles our docs never mention", "should_trigger": true },
    { "query": "How do we tell developers which of our several limits they just hit?", "should_trigger": true },
    { "query": "Retry-After: should we send seconds or an HTTP date?", "should_trigger": true },
    { "query": "Our API signals throttling inside an HTTP 200 response - is that ok?", "should_trigger": true },
    { "query": "Do enterprise customers get published numbers or a contact-sales process for limits?", "should_trigger": true },
    { "query": "We enforce limits independently per region - is that a problem for what we publish?", "should_trigger": true },
    { "query": "Roll out rate limiting on a public API that never had any", "should_trigger": true },
    { "query": "How do we introduce a stricter quota without breaking existing integrations?", "should_trigger": true },
    { "query": "Someone is scraping our public API - should we change our limiting algorithm?", "should_trigger": true },
    { "query": "Free tier abuse is eating our capacity and we need to cut the free limit - how do we announce it?", "should_trigger": true },
    { "query": "What's a reasonable requests-per-minute limit for a B2B SaaS API?", "should_trigger": true },
    { "query": "Design throttling for our multi-tenant platform so one org can't starve the others", "should_trigger": true },
    { "query": "Should our GraphQL mutations cost more points than queries?", "should_trigger": true },
    { "query": "Our docs say 500/min but the gateway enforces 300/min - which one do we fix?", "should_trigger": true },
    { "query": "Write the limits page for our developer docs", "should_trigger": true },
    { "query": "Do we need a secondary abuse limit on top of the main quota?", "should_trigger": true },
    { "query": "How should API limits differ between authenticated and anonymous callers?", "should_trigger": true },
    { "query": "Model our limits on how Stripe and GitHub do rate limiting", "should_trigger": true },
    { "query": "Concurrency limits vs requests-per-second limits for our public API", "should_trigger": true },
    { "query": "Should upgrading to a bigger plan automatically raise a customer's API quota?", "should_trigger": true },
    { "query": "Partners want guaranteed API throughput in the contract - can we promise our rate limit as an SLA?", "should_trigger": true },
    { "query": "We want to change X-RateLimit-Reset from epoch seconds to seconds remaining", "should_trigger": true },
    { "query": "How do clients know how many API requests they have left?", "should_trigger": true },
    { "query": "Add rate limiting to our public API before launch next month", "should_trigger": true },
    { "query": "our api got hammered last night, thinking about throttling rules", "should_trigger": true },
    { "query": "help me pick quotas for free vs paid developers", "should_trigger": true },
    { "query": "We're copying Shopify's leaky bucket for our API - good idea?", "should_trigger": true },
    { "query": "Should test-mode API keys share production rate limits?", "should_trigger": true },
    { "query": "How do we stop developers from discovering our limits only by tripping them?", "should_trigger": true },
    { "query": "Draft the throttling and fair-use section of our API terms", "should_trigger": true },
    { "query": "429 Too Many Requests - design how our API behaves when callers exceed limits", "should_trigger": true },
    { "query": "Points-based rate limiting like GitHub's GraphQL API - how do we set the budget?", "should_trigger": true },
    { "query": "Should the limit apply per API key or per org?", "should_trigger": true },
    { "query": "Weighted fair sharing between tenants instead of hard caps - worth it?", "should_trigger": true },
    { "query": "A customer asked for a temporary limit increase for their launch day", "should_trigger": true },
    { "query": "How do we publish rate limits developers can capacity-plan against?", "should_trigger": true },
    { "query": "Do we owe a changelog entry when we tighten throttling?", "should_trigger": true },
    { "query": "Design the JSON error body our API returns on 429", "should_trigger": false },
    { "query": "What fields belong in our RFC 9457 problem-details envelope?", "should_trigger": false },
    { "query": "Write retry logic with exponential backoff for our Python SDK", "should_trigger": false },
    { "query": "How should idempotency keys work so client retries are safe?", "should_trigger": false },
    { "query": "Set a max query depth and complexity ceiling in our GraphQL schema", "should_trigger": false },
    { "query": "Should we require persisted queries for third-party GraphQL clients?", "should_trigger": false },
    { "query": "How long should our API deprecation window be before we sunset v1?", "should_trigger": false },
    { "query": "When do we send the Sunset and Deprecation headers?", "should_trigger": false },
    { "query": "Configure the rate limiting plugin in Kong for our gateway", "should_trigger": false },
    { "query": "Implement a token bucket limiter in Redis with a Lua script", "should_trigger": false },
    { "query": "What nginx limit_req settings should our reverse proxy use?", "should_trigger": false },
    { "query": "Design our API key rotation policy with dual-key overlap", "should_trigger": false },
    { "query": "Should we store API keys hashed or encrypted?", "should_trigger": false },
    { "query": "Webhook delivery retries and dead-letter policy for failing endpoints", "should_trigger": false },
    { "query": "Define degraded vs partial vs major outage states for our status page", "should_trigger": false },
    { "query": "Where should remaining-quota displays live in our developer portal?", "should_trigger": false },
    { "query": "Design sandbox test-mode isolation so integrators can't touch real data", "should_trigger": false },
    { "query": "Pick OAuth access token TTLs and a refresh rotation policy", "should_trigger": false },
    { "query": "Aggregate 429 error rates per API key into a partner-facing dashboard", "should_trigger": false },
    { "query": "Which languages should get official SDKs first?", "should_trigger": false },
    { "query": "Should we build REST or GraphQL first for our public API?", "should_trigger": false },
    { "query": "My app keeps getting 429s from the Stripe API - how do I handle them?", "should_trigger": false },
    { "query": "How do I get OpenAI to raise my organization's rate limit tier?", "should_trigger": false },
    { "query": "Throttle login attempts to stop credential stuffing on our auth form", "should_trigger": false },
    { "query": "Add CAPTCHA and IP blocking for bot traffic on our marketing site", "should_trigger": false },
    { "query": "Set up Cloudflare WAF rules to mitigate a DDoS on our site", "should_trigger": false },
    { "query": "Set Kubernetes resource quotas per namespace in our cluster", "should_trigger": false },
    { "query": "Request an AWS service quota increase for EC2 vCPUs", "should_trigger": false },
    { "query": "Slow down our outbound email sending so we don't hit spam filters", "should_trigger": false },
    { "query": "Price our API plans - what should the pro tier cost per month?", "should_trigger": false },
    { "query": "Design usage-based billing with metered overage charges", "should_trigger": false },
    { "query": "Debounce the search-box requests in our React frontend", "should_trigger": false },
    { "query": "Set database connection pool limits for our Postgres cluster", "should_trigger": false },
    { "query": "Tune circuit breaker settings between our internal microservices", "should_trigger": false },
    { "query": "Design a backpressure strategy for our Kafka consumers", "should_trigger": false },
    { "query": "How should our mobile app cache API responses to make fewer calls?", "should_trigger": false },
    { "query": "Write the pagination conventions for our public REST API", "should_trigger": false },
    { "query": "Review our REST resource naming and status-code usage", "should_trigger": false },
    { "query": "Design the HMAC signature scheme for our outbound webhooks", "should_trigger": false },
    { "query": "Set up our marketplace review process for third-party apps", "should_trigger": false },
    { "query": "Correlation IDs that survive from response header to support ticket", "should_trigger": false },
    { "query": "Create a Grafana alert when API p99 latency crosses 500ms", "should_trigger": false },
    { "query": "Limit how many rows a customer can export from our web dashboard per day", "should_trigger": false },
    { "query": "Choose the scope taxonomy and consent screen for our OAuth apps", "should_trigger": false },
    { "query": "GitHub Actions concurrency limits are queuing our CI jobs - help", "should_trigger": false },
    { "query": "Design the upgrade-prompt UI copy for users who hit their plan limits", "should_trigger": false },
    { "query": "Write the Terraform for AWS API Gateway usage plans and throttling settings", "should_trigger": false },
    { "query": "Pick the retry budget and timeout propagation strategy for our internal RPCs", "should_trigger": false },
    { "query": "How do we version our webhook payload schema?", "should_trigger": false },
    { "query": "When does my Anthropic API account auto-promote to the next spend tier?", "should_trigger": false },
    { "query": "Plan the canary rollout percentages for deploying our new API version", "should_trigger": false },
    { "query": "Design the partner certification program for our integration directory", "should_trigger": false },
    { "query": "What max request body size should we accept on upload endpoints?", "should_trigger": false }
  ]
}
references/graphql-cost-metering.md›
# GraphQL cost-point metering - formulas, accounting, and client traps

The cost-based metering model in concrete, citable detail: the two dominant formulas (GitHub, Shopify), the smaller named variants, and the traps a policy must design around. Structural query guards (depth ceilings, persisted queries, complexity rejection as schema design) belong to `samber/developer-platform-skills@public-graphql-api-design`; this file covers the pricing and budget policy.

## Why per-request counting fails GraphQL

A REST-style "N requests per minute" treats a query fetching one scalar the same as one fetching hundreds of nested objects, despite wildly different server cost. The cost model prices the _query_, not the _request_: every query gets a computed cost, and the limit is a budget of points per window.

## GitHub GraphQL (v4)

- A connection field's cost is its `first`/`last` argument value ÷ 100, rounded to the nearest whole number, minimum 1 per connection field.
- Schema guards make it ungameable: every connection must supply `first`/`last` in 1–100, and a single call cannot request more than 500,000 total nodes.
- The budget is introspectable mid-session: `rateLimit { limit cost remaining resetAt }` - and querying it costs nothing. A transparency mechanism distinct from a response header, worth copying.
- Budgets: 5,000 points/hr for users, 10,000/hr for Enterprise-owned apps; GitHub App installations scale (50 points/hr per repo and per user above 20, capped at 12,500).
- **Client trap**: exceeding the primary GraphQL limit returns **HTTP 200 with an error in the body** and `x-ratelimit-remaining: 0` - not a 429. A status-code-only client silently treats it as success.

## Shopify Admin API (GraphQL)

Official source: `shopify.dev/docs/api/usage/limits`. Calculated query cost spent against a leaky bucket (50 points/s restore, 1,000-point bucket on Standard; 100/s and 2,000 on Plus - but see the caveat below).

- **Field cost rules**: Scalar = 0, Enum = 0, Object = 1, Connection = sized by its `first`/`last` arguments, Interface/Union = max of its possible selections, **Mutation = 10**. Shopify reserves the right to set manual per-field costs beyond the general rule.
- **Pre-charge then refund**: the bucket must hold the full `requestedQueryCost` (static worst-case estimate) _before_ execution or the query is rejected outright; after execution the difference vs `actualQueryCost` is refunded. Per-response accounting under `extensions.cost`:

```json
{
  "requestedQueryCost": 101,
  "actualQueryCost": 46,
  "throttleStatus": {
    "maximumAvailable": 1000,
    "currentlyAvailable": 954,
    "restoreRate": 50
  }
}
```

- **Portability trap**: a single query may never exceed **1,000 points regardless of plan**, enforced pre-execution on the _requested_ cost. Higher plans get a deeper bucket, not bigger single queries - Shopify's own error: "Query cost is 1040, which exceeds the single query max cost limit (1000)." A query written against a Plus store's bucket can instantly fail on a Standard store.
- **Debug affordance**: `Shopify-GraphQL-Cost-Debug: 1` returns a per-field cost breakdown - the mechanism that lets an integrator understand _why_ a query costs what it costs.
- **Client trap**: a cost-throttled request returns **HTTP 200 with a `THROTTLED` error code** ("Similar to 429 Too Many Requests") - not a 429. Meanwhile Shopify's _resource-based_ limits (e.g. product-creation ceilings) do return a real 429, so one API mixes both conventions by limit type.
- **Conflicting-figures caveat, to state rather than fix silently**: Shopify's official plan-comparison table lists restore rates of 100/200/1,000/2,000 points/s (Standard/Advanced/Plus/Enterprise), while Shopify's own response examples and most 2026 developer write-ups show 1,000-point bucket / 50 points/s (Standard) and 2,000/100 (Plus). The 1,000-point single-query cap is unambiguous across every source; the restore rates are not - verify against the docs for the specific plan rather than trusting either source alone.

## Other named implementations

- **Zonos**: pool refilling at 3,000 points/s; every response includes a `zonos-query-complexity` header stating what the just-executed query cost.
- **Thinkific**: per-minute window; caps a single request at 1,000 points; divides raw cost by 100 purely to keep numbers human-readable.
- **Buildkite**: computes cost both before execution (static estimate, for admission control) and after (based on what was actually returned) - a connection can legitimately return fewer nodes than requested, and the post-execution number is what should count against the budget.

## Policy takeaways

- Publish the cost formula and per-field weights, not just the budget - an integrator who cannot price a query before sending it can only discover the ceiling by tripping it.
- Expose the computed cost back on every response (header or `extensions` object), plus the restore rate and remaining budget, so a client can model the bucket locally.
- Pin the per-query hard cap independently of plan and say so explicitly, or integrators will ship queries that break on smaller-plan customers.
- Weight mutations well above reads of equivalent shape (general guidance 2–5×; Shopify uses 10×).
- REST surfaces on the same platforms stay simple request counters (GitHub 5,000 req/hr; Shopify's 40-request bucket) - the two models coexist per paradigm, they never merge. Shopify is actively deprecating REST in favor of GraphQL, a forward-looking signal for where new metering design effort goes.
references/header-families.md›
# Rate-limit header families - syntax, adoption, and Retry-After mechanics

The wire formats behind the header-family menu, with the cross-vendor evidence for why the legacy family is still the default.

## Legacy family: the `X-RateLimit-*` trio

```
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
```

- `Limit`: ceiling for the current window.
- `Remaining`: calls left in it.
- `Reset`: when the window rolls over.

This is the de facto convention most real APIs ship today, but **reset semantics vary by vendor**: GitHub's `reset` is a UTC epoch timestamp, other vendors use seconds-until-reset. Document which one yours means, and never change it silently - that swap breaks client parsing invisibly instead of producing a visible error.

## IETF family: `RateLimit` and `RateLimit-Policy`

Source: `draft-ietf-httpapi-ratelimit-headers` (IETF httpapi WG; R. Polli, A. Martinez, D. Miller). Current version: **draft-11, dated 23 May 2026, expiring 24 November 2026** - Standards Track intent, still an Internet-Draft, not an RFC. The most recent HTTPDIR early directorate review (filed against draft-10) came back marked "not ready" - an ordinary part of IETF process, but it means field syntax can still change before RFC, reinforcing the caution against treating the field names as settled.

```
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=15;t=23
```

- `RateLimit-Policy` advertises the quota policy itself: `q` = quota units, `w` = window in seconds, optional `pk` = partition key. This is the field that lets a well-behaved client self-throttle before its first 429.
- `RateLimit` reports current state: `r` = remaining quota, `t` = time to reset.
- Multiple simultaneous policies are explicitly supported (an hourly and a daily quota advertised together); the draft also covers concurrency limits, not just request rates.
- Deliberately unspecified: no mandated algorithm, no mandated correlation with status codes, and **no guarantee implied** - the values "are informative and MAY be ignored"; a client seeing "5 remaining" is not promised those 5 calls.
- Syntax churn risk: versions ≤07 used a different `limit=…, remaining=…, reset=…` list syntax; draft-08+ moved to the quoted structured-field form. Track the httpapi WG rather than hard-coding today's field names as permanent.
- The draft's own text concedes that a naive fixed-window reset design "encourage[s] clients into cyclic burst-pause behaviour" - the reset-boundary stampede - which is why published retry guidance still needs jitter even with a standardized header.

**Current adoption reality:**

- Cloudflare adopted the draft in September 2025 - the one named Tier-1 example.
- GitHub still emits only the legacy `X-RateLimit-*` family.
- Stripe emits neither family.

A policy cannot assume clients parse the IETF fields, and a client cannot assume any API emits them.

## `Retry-After` mechanics

- Status code 429 Too Many Requests is defined by RFC 6585 (April 2012), which also **forbids caching a 429** - a caching proxy that ignores this makes the throttle appear to persist longer than intended.
- `Retry-After` has two valid forms per RFC 9110: delay-seconds (`Retry-After: 120`) or an HTTP-date (`Retry-After: Wed, 21 Oct 2026 07:28:00 GMT`). Delay-seconds dominates in APIs and gateways; HTTP-date appears mainly around planned maintenance.
- The pervasive client bug: `parseInt`-ing the header assuming delay-seconds - an HTTP-date parsed that way yields `NaN`, and an unclamped client falls into an instant retry storm. Prefer emitting delay-seconds; your docs should tell clients to detect both forms and clamp to ≥ 0.
- Precedence rule (IETF draft, verbatim): "If a response contains both the RateLimit and Retry-After fields, the Retry-After field MUST take precedence and the effective window MAY be ignored."

## Cross-vendor comparison

| Platform        | Header family                                                                    | Reset semantics                       | Throttle signal                                                    |
| --------------- | -------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------ |
| GitHub REST     | `x-ratelimit-limit/remaining/used/reset/resource` (+ `retry-after` on secondary) | UTC epoch seconds                     | 403 or 429; JSON message; primary → `x-ratelimit-remaining: 0`     |
| GitHub GraphQL  | same headers + `rateLimit{}` body object                                         | epoch seconds                         | primary over-limit → **HTTP 200** with error + `remaining: 0`      |
| Stripe          | `Stripe-Rate-Limited-Reason` only (no `X-RateLimit-*`, no IETF fields)           | n/a                                   | HTTP 429, `type: rate_limit_error` + `code`; SDKs retry internally |
| Shopify REST    | `X-Shopify-Shop-Api-Call-Limit: 40/40`; `Retry-After` on 429                     | n/a (bucket)                          | HTTP 429                                                           |
| Shopify GraphQL | `extensions.cost.throttleStatus` in body                                         | n/a (bucket)                          | **HTTP 200** with `THROTTLED` error code                           |
| SendGrid        | `X-RateLimit-Limit/Remaining/Reset`; `Retry-After` on 429                        | reset window                          | HTTP 429                                                           |
| Discord         | `X-RateLimit-*` + `Reset-After`/`Bucket`/`Scope`/`Global`; `Retry-After`         | epoch seconds + `Reset-After` seconds | HTTP 429, JSON `{message, retry_after}`                            |
| Twilio          | `Twilio-Concurrent-Requests` (concurrency-based)                                 | n/a                                   | HTTP 429, error `20429`                                            |

The load-bearing conclusion: header formats are genuinely incompatible across vendors - three separate legacy headers vs two structured IETF fields vs vendor-unique shapes vs body-only signaling. Whatever family you pick, your published docs are the only thing that makes it parseable on purpose rather than by reverse engineering.

## Implementation notes

- Stripe's exact headers vary by account and should be confirmed empirically: it emits `Stripe-Rate-Limited-Reason` and a `rate_limit_error` body but does not document a `Retry-After` header at all - its posture is "react to 429s, throttle client-side, SDKs retry internally".
- The IETF draft is a distinct standard from RFC 9457 Problem Details: rate-limit state vs generic error-body shape, different layers, not competing options.
- GitHub's secondary-limit point figures (900 points/min REST, 2,000/min GraphQL) are stated directly in GitHub's own rate-limits documentation - cite them as official. The per-endpoint point cost of some individual endpoints stays undisclosed by GitHub's own admission.
references/overrides-and-change-notice.md›
# Override mechanisms and change-notice benchmarks

Named precedent for the two change-facing steps: how real platforms grant limits above the published tiers, and how much notice a limit change gets versus a version deprecation.

## Seven named override mechanisms

The pattern across all of them:

- Self-serve tiers publish exact numbers; the top tier publishes a process.
- Overrides are keyed to the authenticated principal (API key, org, app installation), never a person.
- Every platform reserves the right to lower limits again for stability, regardless of any override granted.

| Platform  | Mechanism                             | Notable detail                                                                                                                                                                                    |
| --------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stripe    | Support ticket                        | Large increases require **≥ 6 weeks' lead time** - the citable planning floor                                                                                                                     |
| Anthropic | Spend-based auto-promotion, Tiers 1→4 | Custom tier beyond Tier 4 arranged with the account team, never self-serve                                                                                                                        |
| GitHub    | Enterprise plan gating                | 15,000 req/hr tied to Enterprise Cloud org membership or org-owned app status; OSS projects occasionally get case-by-case manual whitelisting                                                     |
| Zoom      | Plan gating                           | Higher limits for Business/Enterprise/Education/Partner accounts - not purchasable standalone                                                                                                     |
| Auth0     | **Time-boxed self-serve burst**       | A paid add-on raises the Authentication API limit to a multiplier of the default, but only for up to 48 hours per month - the rare self-serve exception                                           |
| Zyte      | Ticket with structured lead time      | Temporary increases via support ticket ≥ 24h ahead, specifying API key, desired RPM, target sites, dates; permanent increases via sales or account manager - the most process-documented template |
| OpenAI    | Explicit activation                   | Enterprise plan does **not** auto-grant higher limits - they must be configured with OpenAI even after purchase                                                                                   |

Two design details worth copying:

- An override can hold one dimension non-negotiable: GitHub Enterprise Server admins can exempt users from rate limits entirely via `ghe-config`, but the exemption explicitly cannot bypass the separate GraphQL point quota.
- A higher-priced plan does not automatically imply higher limits (OpenAI) - state explicitly in your docs whether yours does.

## Limit change ≠ version deprecation

The industry treats these as two different regimes; keep their numbers separate when citing.

**Rate-limit changes are non-breaking.** IBM's public API policy, verbatim: "The rate limits can be adjusted on API endpoints during the lifetime of the API and does not require a version update. The clients that receive the HTTP status code 429… must make adjustments." Shopify and Stripe both explicitly reserve the right to temporarily reduce limits for platform stability with little or no notice. Consequence: a published rate limit cannot be treated as a contract or SLA - consistent with the IETF draft's insistence that its header values are hints, not guarantees.

**Version deprecation gets formal, long windows** (via `Deprecation`/`Sunset` headers, changelog, targeted email - machinery owned by `samber/developer-platform-skills@api-versioning-policy`). Named benchmarks for the _version_ window, for contrast only:

| Company            | Version-deprecation notice                                                                               |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| Atlassian          | 6 months (standard)                                                                                      |
| IBM                | 12 months                                                                                                |
| Microsoft Graph    | commonly 24 months                                                                                       |
| Salesforce         | ≥ 3-year support per version; retirement waves with 1-year notice; retired versions return HTTP 410 Gone |
| Canvas/Instructure | ≥ 30 days for material policy changes                                                                    |

Practitioner consensus for graduated change rollouts generally:

- ~30 days for minor endpoint changes.
- 60–90 days for major deprecations.
- 6–12 months for full version sunsets.

Run these as a multi-email cadence: announce, remind, warn, throttle, remove. These are the _deprecation_ numbers; a rate-limit change sits below all of them, which is exactly why it needs its own written notice policy - nothing else forces communication.

## Rate-limit-specific change practice

- Version the _policy document_ alongside the API; publish burst, sustained, and the algorithm in use.
- Announce reductions with lead time proportional to blast radius. Stripe's ≥ 6-week guidance for large _increases_ is a reasonable floor to apply symmetrically to _decreases_.
- Introduce or tighten limits on live traffic via GitHub's admin rollout sequence, communicating with affected callers before each tightening:
  1. **Observe** - log real traffic, no enforcement.
  2. **Baseline** - enforce at a high initial ceiling.
  3. **Refine** - tighten from observed data.
- Expose limits in-band via headers so well-behaved clients adapt automatically rather than needing to read a changelog.
- **Never silently change reset semantics** (e.g. epoch-seconds → seconds-remaining): that change breaks client parsing invisibly instead of producing a visible error - it deserves a major announcement on its own.
- Zuplo's line for the policy's tone: "provide ample notice and explain the reasons for the changes to maintain transparency and trust."
references/vendor-limit-models.md›
# Vendor limit models - what real platforms key their limits on

Evidence base for the tier-design step: which named platforms key limits on what, how they split burst from sustained, and which of their figures to trust.

## The tier-keying finding

The naive mental model - a billing-plan column maps to a limit column - is the special case among large platforms, not the default:

| Platform  | Primary limit keyed on                                                                                                                    | Evidence                                                                                                        |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Stripe    | Flat account default + per-API limits + support-granted overrides - **not plan**                                                          | Docs state "API endpoints have a default limit of 25 requests per second"; live mode 100 operations, sandbox 25 |
| GitHub    | **Auth method**: 60 req/hr unauthenticated (per IP), 5,000 req/hr per user/PAT/OAuth token, 15,000 req/hr for Enterprise Cloud org tokens | GraphQL metered separately in points                                                                            |
| Shopify   | Plan: REST standard = 40-request bucket leaking 2/s, Plus = 400 leaking 20/s                                                              | Leaky bucket, vendor's own marble metaphor                                                                      |
| Zoom      | Plan × request-type: endpoints grouped Light/Medium/Heavy/Resource-intensive, quotas per Free/Pro/Business+                               |                                                                                                                 |
| Auth0     | Tenant type (production vs dev) × subscription (Free/Essential/Professional/Enterprise)                                                   |                                                                                                                 |
| Anthropic | Cumulative spend: auto-promotion through Tiers 1→4, Custom via account team                                                               |                                                                                                                 |

Design consequence: model limits as a function of **identity/auth-scope × endpoint-class × account-override**, and make plan-tiering a deliberate choice on top, not the starting assumption.

## Per-endpoint-class granularity (Stripe)

Rather than one account-wide number, Stripe publishes separate limits per API surface:

- Files API: 20 read + 20 write/s.
- Search API: 20 read/s.
- Payment Intents: 1,000 updates per intent per hour.
- Payouts: 15 req/s + 30 concurrent per merchant.
- Meter events: 1,000 calls/s per account.
- Connect account creation: 30/s live.

This is the pattern for carving expensive operations out of the general-purpose budget.

## Primary/secondary split (GitHub)

A two-layer policy distinct from a flat single limit:

- **Primary**: the headline quota by auth tier (60 / 5,000 / 15,000 req/hr).
- **Secondary**: an abuse guard triggered by request _shape_, not raw count - no more than 100 concurrent requests (shared across REST and GraphQL). GitHub's own docs now officially state the ceiling: no more than 900 points per minute for REST endpoints, no more than 2,000 points per minute for the GraphQL endpoint. **Cite the 900/2,000 ceilings as official** - what stays undisclosed is the per-endpoint point cost, which GitHub's docs say some endpoints deliberately do "not share publicly".

The communication lesson: a caller can be under its published quota and still get throttled by an opaque layer it discovers only by tripping it. Have a secondary guard - but document that it exists, what shape triggers it, and how it signals.

## Burst vs sustained as two explicit numbers

- **Shopify** communicates both live in every GraphQL response: `throttleStatus.maximumAvailable` (burst depth), `currentlyAvailable` (remaining), `restoreRate` (sustained points/s).
- **Stripe** engineered burst headroom deliberately - its engineering blog (Paul Tarjan, "Scaling your API with rate limiters"): Stripe "added the ability to briefly burst above the cap for sudden spikes in usage during real-time events". Sustained = token refill rate, burst = bucket depth, both intentional.
- **GitHub** splits them into two _different mechanisms_ (hourly primary quota + secondary burst guard) - workable, but only if both are documented; see above.
- The widely-cited failure mode is **burst = sustained**, which breaks legitimate spikes (webhook fan-out, scheduled reports) that need headroom above steady state. Setting burst far above sustained, conversely, defeats the protection the sustained rate exists to provide.

## A minimal published tier table

The shape integrators expect on a limits page - adapt the numbers, keep the columns:

| Tier          | Limit      | Window / keyed on | Use case            |
| ------------- | ---------- | ----------------- | ------------------- |
| Anonymous     | 30/min     | Per IP            | Public endpoints    |
| Authenticated | 100/min    | Per user          | Standard API access |
| Paid          | 1,000/min  | Per API key       | Paid plans          |
| Internal      | 10,000/min | Per service       | Service-to-service  |

Two choices this table makes implicitly that a policy must make explicitly: the identifier a tier keys on changes with the tier (IP → user → API key → service), and the magnitude step between tiers (~30× here) is a decision, not a constant. State both on purpose.

## Standing caveats

- Published limits are hints, not SLAs: every major platform in the evidence base reserves the right to lower limits for platform stability, sometimes without notice.
- Stripe's exact per-account behavior must be confirmed empirically - its docs describe defaults, and increases are per-account via Support.
- Twilio is the reminder that the metering unit itself varies: its throttling is concurrency-based (`Twilio-Concurrent-Requests`, error 20429) - in-flight requests, not requests-per-window.
SKILL.md›
---
name: api-rate-limit-policy
description: Design the rate-limit policy a public API publishes to its consumers - the metering model per paradigm (REST request counting vs GraphQL cost points), tier and quota-vs-burst numbers, multi-tenant fairness, rate-limit headers (legacy X-RateLimit-* vs IETF RateLimit fields), 429 and Retry-After behavior, enterprise and partner overrides, and change-notice rules. Use whenever the user mentions rate limits, quotas, throttling tiers, 429 responses, noisy neighbors, or limit-increase requests - even if they never say "rate-limit policy". Policy layer only, not gateway configuration. Do NOT use for the 429 error envelope - use samber/developer-platform-skills@api-error-design instead.
license: MIT
metadata:
  author: Samuel Berthe
  version: "1.0.1"
---

# API Rate Limit Policy

You are an API rate-limit policy designer. Design the limits a public API publishes to its consumers - the metering model, the tier and burst numbers, the fairness rules, the headers, the throttle response, the override path, and how changes land - so an integrator can predict, observe, and adapt to throttling instead of discovering each ceiling by tripping it. Which gateway or middleware enforces the limit is out of scope; what the limit promises and how it is communicated is the whole scope.

Zuplo's one-line purpose statement is the mission: rate limiting sets "a policy of fair access to API resources and prevent[s] any single user or application from consuming excessive resources and impacting the experience of others." And GeekyAnts' framing is why this is policy work, not plumbing: "designing a rate limiter is fundamentally a strategic decision about fairness, scalability, and user experience."

**Memory (advised):** When memory lives in a file, consider using `developer-platform-context.md`; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.

## Clarifying questions

Ask these before designing anything; each answer feeds a numbered step. Batch them - this is a tactical design task, not a strategy interview.

1. Paradigm: REST-only, GraphQL-only, or both? (Request counting and cost points are incompatible metering models; step 1 picks per surface.)
2. Greenfield, or limits already enforced? If existing: request the currently enforced numbers and the currently documented numbers - drift between the two is the first finding.
3. What consumer tiers exist or are planned (free / self-serve paid / enterprise-partner), and are enterprise customers already asking for higher limits?
4. Abuse pressure: is the API scraped or hit adversarially, or is traffic mostly well-behaved integrations? (Promotes the sliding-window rung and a secondary guard in step 1.)
5. Has one customer's traffic ever degraded others' latency? (Raises the priority of step 3's fairness layers.)
6. Is the API served from more than one region? A limit enforced independently per region multiplies a caller's real global throughput by the region count - cited as "not theoretical, it's a real exploit vector" - so the published limit must state its scope.
7. What can the edge/gateway already emit? (An edge that produces IETF structured fields cheaply re-ranks the header menu in step 4; a gateway that can't count cost points constrains step 1.)
8. By when must the published policy land, is this a one-off fix (unblock one enterprise negotiation) or a compounding contract the platform operates for years, and what is the effort ceiling - engineering hours for limiter work, an on-call rotation to hold fairness layers, appetite for a consumer-visible change?

Question 8 exists because the algorithm menu (step 1) and the header menu (step 4) diverge sharply on time-to-effect, durability, and effort; neither ranking can be picked without it. Each answer moves a different rung:

- A hard date promotes whatever the current gateway already enforces, and the legacy header trio.
- A compounding mandate promotes the sliding window and the both-families rung.
- A low effort ceiling deletes the fairness layers of step 3 from this pass rather than parking them at the bottom, with "the first noisy-neighbour incident" as their unblocking condition.

## Consumer tiers

The consumer tier is the axis that changes this design, and it is not a mere segmentation: the tier ladder **is** the policy artifact this skill produces.

- **Anonymous/free callers** - keyed per IP or per free key; the tightest numbers, the first shed under load. They learn their limits from headers alone.
- **Self-serve paid integrators** - keyed per API key or org; published exact numbers they can capacity-plan against. They need a documented table.
- **Enterprise and partners** - published process instead of published numbers; overrides keyed to the authenticated principal (step 6). They need a named override path with lead times.

## Workflow

1. Pick the metering model per paradigm - and the algorithm, which is what the number promises.
2. Design tiers: quota and burst as two numbers, keyed deliberately.
3. Layer fairness and tenant isolation.
4. Choose the header family.
5. Design the throttle response: 429 and Retry-After.
6. Define the enterprise/partner override path.
7. Write the change-notice policy.

Each step has a section below, in order.

## 1. Pick the metering model

Two incompatible models exist, split by paradigm - never blend them:

- **REST: count requests.** GitHub REST is a flat 5,000 req/hr; Shopify REST is a 40-request bucket leaking 2/s. Simple, universally understood, blind to per-request cost.
- **GraphQL: price the query in cost points.** A one-scalar query and a 100-connection query differ enormously in server cost; the limit is a budget of points per window, with a published cost formula (GitHub: connection `first`/`last` ÷ 100, minimum 1; Shopify: scalar 0, object 1, mutation 10, pre-charge then refund). You own the pricing and budget policy; structural guards (depth ceilings, persisted queries) are schema design owned by `samber/developer-platform-skills@public-graphql-api-design`. See [references/graphql-cost-metering.md](references/graphql-cost-metering.md).
- When both surfaces exist, publish both models separately and state whether they share anything (GitHub: separate budgets, but one shared 100-concurrent-request secondary cap).
- Name the metering unit even when it is neither: Twilio meters concurrency - in-flight requests, not requests-per-window - a fundamentally different unit a caller must plan differently for.

The algorithm underneath is a policy choice too, because it defines what the published number promises (burst behavior, boundary behavior). Four options, ranked:

- efficiency: `token bucket > fixed window > sliding window > leaky bucket`
- effort: `sliding window > token bucket == leaky bucket > fixed window`
- value (public developer API): `token bucket > sliding window > leaky bucket > fixed window`

- **Default rung: token bucket** - bucket depth is the burst, refill rate is the sustained quota, so step 2's two numbers fall out of the algorithm for free. It rewards well-behaved bursty clients, it is Stripe's and GitHub's choice, and convergent vendor guidance (Stripe, Google Cloud, AWS API Gateway) calls it "the standard choice for most web services… start here unless you have a specific reason not to." The token-bucket == leaky-bucket effort tie is real: both maintain one bucket per key; they differ in intent, not implementation weight.
- **Step down to fixed window** only for internal or low-stakes limits where the documented flaw - up to 2× the limit at the window boundary - is harmless (Sentry accepts exactly this trade for simplicity). Its second flaw is the stampede: throttled clients synchronize their retries at the reset boundary, a failure mode the IETF draft itself acknowledges.
- **Promote to sliding window** under adversarial pressure (question 4). This is the starved option: boundary-proof and fairest, with the most precise `Retry-After`, but the highest compute cost - it loses every efficiency round, and scraping/abuse pressure is what promotes it anyway.
- **Choose leaky bucket when a burst is a threat, not a feature** - a fragile downstream or fan-out delivery. Shopify, a payments-adjacent platform, still chose it because flash-sale webhook fan-out dominated: "Each app has access to a bucket… Each second, a marble is removed."
- Naming trap: "token bucket" and "leaky bucket" get inverted informally more than any other pair in this space. In the published policy, describe the behavior (does idle time bank burst capacity, or does output stay constant?), never rely on the name alone.
- This is not one skill-wide decision: Stripe runs four limiter layers simultaneously (step 3), each with its own mechanism. The ranking is a default, not a law - re-rank against the interview answers and what you know of the user's stack; a gateway with a built-in sliding-window limiter gets that rung nearly free, which flips the effort line.

## 2. Design tiers: two numbers, keyed deliberately

- The plan→limit table is the special case among real platforms, not the default:
  - Stripe does not tier by plan at all - flat account defaults, per-API limits, support-granted overrides.
  - GitHub keys its primary limit off auth method: 60 unauthenticated / 5,000 per token / 15,000 Enterprise-org.
  - Shopify, Zoom, Auth0, and Anthropic do map limits to plan.
- Model limits as **identity/auth-scope × endpoint-class × account-override**, then decide deliberately whether plan is one of the inputs. See [references/vendor-limit-models.md](references/vendor-limit-models.md).
- Publish burst and sustained as **two explicit numbers**, never one blended figure. Burst = sustained is the widely-cited failure mode - it breaks legitimate spikes (webhook fan-out, scheduled reports) that need headroom above steady state; Stripe engineered its burst allowance deliberately and says so.
- Let the identifier a tier keys on change with the tier: IP for anonymous, user/token for authenticated, API key or org for paid, service identity for internal. Each step up the ladder is a policy decision to state, not an accident.
- Carve expensive operations out of the general budget with per-endpoint-class limits (Stripe publishes separate limits per API surface: Files 20 read/s, Search 20 read/s, Payouts 15 req/s + 30 concurrent).
- A secondary abuse guard - triggered by request shape (concurrency, per-endpoint pressure) rather than raw count - is legitimate and often necessary. But document that it exists and how it signals: GitHub's deliberately under-documented secondary limit is the named communication failure, where a caller under its published quota is throttled by an opaque layer it discovers only by tripping it.
- Decide deliberately whether a test/sandbox mode gets its own tier, and default to no: reusing the production numbers in test mode keeps integrators' throttling code honest, and several mature platforms do exactly that. Add a separate pre-production quota only against evaluation-volume abuse (free-tier scraping, load tests on a shared sandbox), shaped as an aggregate volume-and-scope cap rather than a faster per-minute limiter. `samber/developer-platform-skills@api-test-mode-design` decides whether that quota exists at all and carries the per-vendor postures; you design the tier once it does.
- State the published numbers as operational hints, not an SLA - every major platform reserves the right to lower limits for stability (step 7 owns how that lands).

## 3. Layer fairness and tenant isolation

Fairness is a policy decision, not a side effect of a good algorithm: the same token bucket applied to one shared pool lets the loudest tenant win by accident. The canonical incident is one tenant's runaway 2am batch job degrading every other tenant's latency while staying under any single cap.

1. **Tenant-level ceiling** - the overall limit per customer, where the tier table from step 2 applies.
2. **Per-user limit inside the tenant's allocation** - stops one user of a multi-seat account from spending the whole tenant budget.
3. **Per-endpoint-class carve-out** - a small allocation for expensive operations, independent of the general budget.

- Where a hard per-tenant cliff is too brutal, use weighted sharing instead: each tenant gets a weight (enterprise 10× a free tenant's share) and capacity divides proportionally, so a small tenant is never starved to zero when a large one saturates the pool. Cohere's production version combines tier weights with Deficit Round Robin and priority queues. The trade: you give up a deterministic guaranteed rate to eliminate the fully-blocked-small-tenant cliff.
- AWS's Builders' Library states the goal: with per-tenant quotas, "only the unplanned portion is rejected while other workloads continue with predictable performance" - and names the paradox to accept knowingly: a quota protects everyone else's availability while reducing the throttled tenant's own, even when spare capacity existed.
- Defense-in-depth is the mature end state. Stripe runs four limiter types simultaneously:
  - A request rate limiter - "by far the most important one".
  - A concurrency limiter for expensive endpoints.
  - A fleet load shedder reserving ~20% of capacity for critical methods.
  - A worker-utilization shedder that sheds by priority class: test-mode traffic first, then GETs, then POSTs.
- Load shedding is a different mechanism from rate limiting - it keys off overall system state, not the caller's own behavior, so a well-behaved tenant can still be shed under fleet stress. Say so in the published policy; a fairness promise that omits this overclaims.
- Internal fairness machinery (WFQ, DRR, shedders) is architecture, not published policy - document its externally observable effects (what a caller can see and when), not the mechanism.

## 4. Choose the header family

Two live options for the machine-readable limit state on responses, ranked. A third - emitting the IETF fields _alone_ - is deleted from the menu below rather than ranked last, so it cannot creep back in as scope:

| Option                      | Efficiency | Effort | Value | Durability |
| --------------------------- | ---------- | ------ | ----- | ---------- |
| legacy `X-RateLimit-*` trio | `+++`      | `++`   | `+`   | `++`       |
| both families               | `-`        | `--`   | `+++` | `--`       |

- **Default rung: the legacy trio** - `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`. It is what nearly every client and SDK already parses; GitHub still emits only this family. Document your `Reset` semantics explicitly (epoch timestamp vs seconds-remaining - vendors genuinely differ).
- **Promote to both families** - the starved option, highest value and highest effort - when the edge emits IETF structured fields cheaply (question 7) or when advertising the policy itself matters: `RateLimit-Policy` lets a well-behaved client self-throttle before its first 429, which the legacy trio cannot express. Cloudflare adopted both header families in September 2025 - production traction worth citing from a Tier-1 platform.
- **Deleted from the menu, not ranked last: IETF fields alone.** Shipping them as your only limit-state signal breaks the clients you already have. Adoption is thin: GitHub and Stripe emit no IETF fields at all. The spec (`draft-ietf-httpapi-ratelimit-headers`) is Standards Track but pre-RFC, and has already changed syntax across draft versions, so today's field names can move under you.
- **Re-promotion trigger:** put this option back on the menu when the draft reaches RFC status, or when a second Tier-1 platform adopts it. Until then the IETF fields ship only inside the both-families rung - frame them as forward-looking in your docs, and cite them by their current names (`RateLimit`, `RateLimit-Policy`).
- Whatever you emit, headers are advisory hints, never guarantees - the draft itself says the values "are informative and MAY be ignored", and "5 remaining" does not promise five successful calls. Publish them as planning signals, not commitments.
- One posture sits outside this menu rather than below it: emitting no limit-state headers at all, Stripe's choice - react to 429s, throttle client-side. It is viable only when you also ship SDKs that retry internally, and it must read in the docs as a deliberate decision, never as an omission.
- This ranking is a default, not a law - re-rank against question 7 and the clients you actually have: an audience that is 90% your own SDKs weakens the legacy trio's compatibility argument, since you control both ends of the parse and can move the whole fleet in one SDK release.

See [references/header-families.md](references/header-families.md) for exact syntax, the cross-vendor comparison table, and sourcing caveats.

## 5. Design the throttle response

- Every throttle on a REST surface returns **HTTP 429** (RFC 6585) with a `Retry-After` header - no exceptions. RFC 6585 also forbids caching a 429; a caching layer that ignores this stretches the apparent throttle beyond its real duration.
- `Retry-After` has two valid RFC 9110 forms - delay-seconds and HTTP-date. Emit delay-seconds: the pervasive client bug is `parseInt`-ing an HTTP-date into `NaN` and retry-storming. The IETF draft's precedence rule applies whatever else you emit: `Retry-After` MUST take precedence over any computed reset.
- The 429 body is a plain instance of the API's error envelope - a machine-readable code such as `rate_limit_exceeded` - designed by `samber/developer-platform-skills@api-error-design`; never invent a rate-limit-specific format. This skill decides which signals appear (retry timing, and _which_ limit was hit - Stripe's `Stripe-Rate-Limited-Reason` is the model for disambiguating multiple limits); that skill owns the envelope's shape.
- **The named trap to design out: a throttle inside HTTP 200.** Shopify GraphQL returns a `THROTTLED` error code in a 200; GitHub GraphQL's primary limit returns 200 with an error body and `x-ratelimit-remaining: 0`. A status-code-only client treats both as success. If GraphQL's partial-result semantics force a body-level signal, document it as loudly as a status code, keep it machine-readable, and never mix conventions silently - Shopify returns 200-`THROTTLED` for cost limits but real 429s for resource limits, two cases a client must learn separately.
- Publish retry guidance next to the limits:
  - Honor `Retry-After`.
  - Back off exponentially **with jitter** - Marc Brooker, AWS: "The solution isn't to remove backoff. It's to add jitter."
  - State the consequence for hammering - GitHub: continuing while limited "may result in the banning of your integration".
- The client-side mechanics - backoff algorithms, retry budgets, idempotency keys - belong to `samber/developer-platform-skills@api-idempotency-retry`; you publish the expectations, it designs the implementation.

## 6. Define the enterprise/partner override path

- The cross-vendor pattern: self-serve tiers publish exact numbers; the top tier publishes a **process**. Overrides go through support, sales, or an account manager - almost never a dashboard toggle - and are keyed to the authenticated principal (API key, org, app installation), never a person.
- Pick which mechanism you are modeling and document it; "contact sales" with no process is the gap. Four named shapes exist:
  - Ticket-based with lead time - Zyte: ≥ 24h ahead, naming key, RPM, dates; Stripe: ≥ 6 weeks for large increases.
  - Spend-based auto-promotion - Anthropic's Tiers 1→4.
  - Plan-gated - Zoom, GitHub Enterprise.
  - Time-boxed self-serve burst - Auth0's 48-hours-per-month multiplier, the rare self-serve exception.
- State explicitly whether a higher-priced plan implies higher limits - OpenAI's Enterprise plan does not until limits are explicitly activated, which contradicts most buyers' assumption.
- An override may hold one dimension non-negotiable (GitHub's server-admin exemption still cannot bypass the GraphQL point quota) - decide which of your limits no override touches, and say so.
- Reserve in writing the right to lower any override for platform stability.

See [references/overrides-and-change-notice.md](references/overrides-and-change-notice.md) for all seven vendor mechanisms.

## 7. Write the change-notice policy

- The industry treats a limit-number change as **non-breaking** - IBM states it plainly: rate limits "can be adjusted on API endpoints during the lifetime of the API and does not require a version update." No version bump means no deprecation machinery fires, which is exactly why a written notice policy matters: nothing else forces communication. Keep the two regimes separate - version deprecation (6–24-month windows, `Deprecation`/`Sunset` headers) belongs to `samber/developer-platform-skills@api-versioning-policy`.
- Scale notice to blast radius. Stripe's ≥ 6-week lead time for large limit _increases_ is a reasonable symmetric floor for _decreases_.
- Introduce or tighten limits on live traffic via GitHub's own rollout sequence, communicating with affected callers before each tightening:
  1. Observe - log real traffic with no enforcement.
  2. Baseline - enforce at a high ceiling.
  3. Refine - tighten from observed data.
- Emergency reductions for platform stability are legitimate; reserve them in writing (Shopify and Stripe both do) so they are policy, not surprise.
- **Never silently change reset semantics** (epoch-seconds → seconds-remaining): it breaks client parsing invisibly instead of producing a visible error - the one limit change that warrants a major announcement by itself.
- In-band beats changelog: limits exposed via headers let well-behaved clients adapt automatically. Zuplo sets the tone for the rest: "provide ample notice and explain the reasons for the changes to maintain transparency and trust."

## Failure modes

Anti-pattern checklist - each is a direct audit finding:

- A throttle signaled inside HTTP 200 that the docs never mention.
- Burst set equal to sustained, or only one blended number published.
- One shared bucket for all tenants - noisy neighbor by design.
- A per-region limit presented as global (real throughput = limit × regions).
- A secondary/abuse limit whose existence is discoverable only by tripping it.
- A documented limit that differs from the enforced one, in either direction.
- Reset semantics changed silently.
- A 429 without `Retry-After`, or `Retry-After` emitted as an HTTP-date when delay-seconds would do.
- A policy that names token bucket but describes leaky-bucket behavior, or vice versa.
- An override granted verbally, keyed to nothing, documented nowhere.
- A published limit cited in a contract as an SLA commitment without the policy saying it is one.

## Measurement

Two gates; iterate the design until both pass.

- **Machine-readability: 100% of throttle responses carry a machine-readable signal a documented client can branch on** - a 429 with `Retry-After`, or a documented in-body code where paradigm constraints force a 200. A single undocumented 200-throttle path fails the gate, because a client can only be as correct as the worst-documented signal.
- **Documentation-enforcement parity: the published limits are the enforced limits.** Diff the docs against the enforcing configuration both directions: every documented number matches what is enforced, and every enforced limit - secondary guards included, at least their existence and trigger shape - appears in the docs. Zero drift.

Trends to baseline from your own first month (no industry thresholds exist):

- Per-tier 429 rate.
- Share of 429s followed by a retry inside the `Retry-After` window - clients not honoring it is a docs or SDK gap.
- Limit-related support tickets.
- Override requests per quarter - persistent volume means the published tiers are set too low.

## Invocation examples

- "Design rate-limit tiers for our public REST API - we have free, pro, and enterprise plans, and enterprise keeps asking for higher limits."
- "We're launching a public GraphQL API next to our REST API - how should rate limiting work across the two?"
- "Our biggest customer's batch jobs degrade everyone else's latency at night - design fairness into our limits without hard-blocking them."
- "Audit our rate-limit docs against what the gateway actually enforces, and pick the headers we should return."

## References

- [references/vendor-limit-models.md](references/vendor-limit-models.md) - what named platforms key limits on, per-API granularity, primary/secondary splits, burst-vs-sustained numbers, a minimal tier table.
- [references/header-families.md](references/header-families.md) - legacy and IETF header syntax, `Retry-After` mechanics, cross-vendor comparison, implementation notes.
- [references/graphql-cost-metering.md](references/graphql-cost-metering.md) - GitHub and Shopify cost formulas, pre-charge/refund accounting, the 1,000-point portability trap, policy takeaways.
- [references/overrides-and-change-notice.md](references/overrides-and-change-notice.md) - seven named override mechanisms, the limit-change vs version-deprecation distinction, rollout and notice benchmarks.

See also, same collection:

- `samber/developer-platform-skills@developer-portal-design` - where published limits, usage dashboards, and remaining-quota displays surface in the developer portal.