API Rate Limiting Algorithms, 429 Responses and Headers

Compare API rate limiting algorithms, return 429 the way RFC 6585 describes, and advertise quotas with the IETF RateLimit headers in OpenAPI.

On this page

The four common API rate limiting algorithms are fixed window, sliding window (log or counter), token bucket and leaky bucket. Token bucket suits most public APIs because it allows short bursts while holding an average rate. Sliding window counter gives smoother limits than fixed window for almost the same storage. Whichever you pick, reject excess requests with 429 Too Many Requests from RFC 6585, include a Retry-After header, and tell clients their quota up front with the RateLimit and RateLimit-Policy fields from the IETF draft. Then document all of it in your OpenAPI spec so client authors can plan for it.

How each algorithm counts requests

Every rate limiter answers one question per request: has this client used up its allowance? The algorithms differ in how they define "allowance" over time, what state they store per client, and how they behave at the edges.

Fixed window

Keep one counter per client per clock window, such as 100 requests per minute, and reset it when the minute rolls over. It needs one integer and one expiry per client, and it is easy to explain to customers. Its weakness is the boundary. A client can send 100 requests at 12:00:59 and another 100 at 12:01:00, so the server sees 200 requests in two seconds while the client never broke the stated limit. If your backend cannot absorb twice the limit in a burst, fixed window is the wrong choice.

Sliding window log and sliding window counter

A sliding window log stores a timestamp for every accepted request and counts the ones inside the last 60 seconds. It is exact, and it costs memory in proportion to the limit: a 10,000-per-hour plan means up to 10,000 timestamps per client. The sliding window counter approximates the same result with two counters. It weights the previous window's count by how much of that window still overlaps the last 60 seconds, then adds the current count.

With a limit of 100 per minute, 80 requests in the previous minute, 30 so far in the current one, and 15 seconds into the current minute, 45 of the previous 60 seconds still count. The estimate is 80 x 0.75 + 30 = 90, so the client has 10 requests left. The estimate assumes the previous window's requests were spread evenly, which is the trade you make for constant storage.

Token bucket

A bucket holds up to a fixed number of tokens and refills at a steady rate. Each request removes one token, or more for expensive operations; when the bucket is empty, the request is rejected. Capacity sets the largest burst and the refill rate sets the long-run average, so you can allow a client that has been idle to send 50 requests at once while still capping it at 10 per second over time. State per client is a token count and a timestamp.

JavaScripttoken-bucket.mjs
export class TokenBucket {
  constructor(capacity, refillPerSecond) {
    this.capacity = capacity;
    this.refillPerSecond = refillPerSecond;
    this.tokens = capacity;
    this.updatedAt = Date.now();
  }

  take(cost = 1) {
    const now = Date.now();
    const elapsed = (now - this.updatedAt) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSecond);
    this.updatedAt = now;

    if (this.tokens >= cost) {
      this.tokens -= cost;
      return { allowed: true, remaining: Math.floor(this.tokens) };
    }
    const retryAfter = Math.ceil((cost - this.tokens) / this.refillPerSecond);
    return { allowed: false, remaining: 0, retryAfter };
  }
}

// 50-request burst, 10 requests per second on average
const bucket = new TokenBucket(50, 10);
console.log(bucket.take()); // { allowed: true, remaining: 49 }
In-memory, one process. With several API instances, keep the count and timestamp in a shared store and update them atomically, or each instance enforces its own separate limit.

Charging by cost matters for batched requests. OWASP's API4:2023 entry on unrestricted resource consumption describes an attacker who sends many upload operations in one batched GraphQL request to get around a per-request limit. Charging tokens by work done, rather than by HTTP request, closes that gap.

Leaky bucket

Requests enter a queue that drains at a fixed rate; when the queue is full, new requests are dropped. The output is perfectly smooth, which protects a downstream service that cannot take bursts at all, such as a legacy system or a third-party API with its own strict limit. The cost is latency: a request at the back of the queue waits, and clients see slow responses before they see rejections.

AlgorithmState per clientBurstsMain failure mode
Fixed window1 counterUp to 2x the limit across a window boundaryBoundary spikes
Sliding window log1 timestamp per requestNone beyond the limitMemory grows with the limit
Sliding window counter2 countersClose to noneApproximation when traffic is uneven
Token bucketToken count and timestampUp to bucket capacityLarge capacity lets an idle client spike
Leaky bucketA queueAbsorbed by queueingAdded latency, then drops

Picking an algorithm

ScenarioRecommendationAlternative
Clients send bursts (page loads, batch jobs) and your backend can absorb themToken bucket, with capacity set to the largest burst you acceptSliding window counter if you prefer a hard per-window number
You publish a simple "N per minute" plan and need predictable enforcementSliding window counterFixed window only if 2x boundary bursts are harmless
A downstream dependency cannot handle any burstLeaky bucket in front of that dependencyToken bucket with a small capacity
Each request needs exact accounting, such as paid metered callsSliding window log, if the memory per client is acceptable—

What to count against: key and cost

RFC 6585 leaves this open: it does not define how the server identifies the client or how it counts requests. Pick the key that matches who pays for the traffic. An API key or OAuth client ID fits partner APIs. A user ID fits first-party apps where one customer runs many devices. An IP address is a fallback for unauthenticated routes, and a poor one, because many users can share one address behind a corporate NAT. Most APIs need more than one limit: a per-key quota for fairness and a stricter per-route limit on expensive or abusable operations. OWASP's API4:2023 scenario of an attacker triggering a paid SMS on a forgot-password endpoint again and again is a case for a per-route limit keyed on the target phone number, not only on the caller.

The 429 response

RFC 6585 defines 429 Too Many Requests for exactly this case. The response should include details explaining the condition and may include Retry-After; caches must not store it. RFC 9110 defines Retry-After as either an HTTP-date or a number of seconds. Seconds are easier for clients and avoid clock skew. Use 503 Service Unavailable instead when the whole service is overloaded, not one client; the HTTP status codes reference lists both. For the body, a problem details object keeps rate-limit errors in the same shape as the rest of your errors, as the error handling guide recommends.

HTTP429-response.http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 41
RateLimit-Policy: "burst";q=100;w=60
RateLimit: "burst";r=0;t=41

{
  "type": "https://api.example.com/problems/rate-limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "The burst policy allows 100 requests per 60 seconds. Try again in 41 seconds."
}

The IETF RateLimit and RateLimit-Policy headers

Retry-After only arrives after the client is already blocked. The IETF HTTPAPI working group's draft, draft-ietf-httpapi-ratelimit-headers (version 11, May 2026), adds two fields that let clients slow down before that. RateLimit-Policy describes a quota: q is the quota in units, w the window in seconds, qu the unit type (requests by default, or content-bytes or concurrent-requests), and pk an optional partition key. RateLimit reports the current state of a named policy: r is the remaining quota and t the seconds until it resets. In the example above, the burst policy allows 100 requests per 60 seconds and the client has 0 left for 41 more seconds.

  • Send both fields on successful responses too, not only on 429, so clients can pace themselves.
  • The draft says that when a response has both RateLimit and Retry-After, Retry-After takes precedence, and Retry-After should not point to a time earlier than the end of the effective window.
  • It is still an Internet-Draft, not an RFC. Earlier versions used separate RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset fields, and many APIs still send vendor X-RateLimit-* headers. Document exactly which fields you send and keep the policy names stable.

Document limits in your OpenAPI spec

Define the headers once under components.headers, put the 429 response under components.responses, and reference both from each operation. Generated clients and docs then show the limit next to the operation instead of in a separate wiki page.

YAMLreports-api.yaml
openapi: 3.0.3
info:
  title: Reports API
  version: 1.0.0
paths:
  /v1/reports:
    get:
      summary: List reports
      responses:
        "200":
          description: A page of reports
          headers:
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimit-Policy"
            RateLimit:
              $ref: "#/components/headers/RateLimit"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /v1/report-exports:
    post:
      summary: Start a report export job
      responses:
        "202":
          description: Export accepted
          headers:
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimit-Policy"
            RateLimit:
              $ref: "#/components/headers/RateLimit"
        "429":
          $ref: "#/components/responses/TooManyRequests"
components:
  headers:
    RateLimit-Policy:
      description: Quota policies for this client, for example "burst";q=100;w=60
      schema:
        type: string
    RateLimit:
      description: Remaining quota and seconds until it resets, for example "burst";r=12;t=41
      schema:
        type: string
    Retry-After:
      description: Seconds to wait before retrying
      schema:
        type: integer
        minimum: 0
  responses:
    TooManyRequests:
      description: Rate limit exceeded
      headers:
        Retry-After:
          $ref: "#/components/headers/Retry-After"
        RateLimit:
          $ref: "#/components/headers/RateLimit"
      content:
        application/problem+json:
          schema:
            type: object
            required: [type, title, status]
            properties:
              type:
                type: string
              title:
                type: string
              status:
                type: integer
              detail:
                type: string

This spec scores 100 on APIForge's design score, and so does a copy with every 429 response and rate-limit header removed and a plain 400 response in their place. The score checks that error responses exist, not which ones, so a missing 429 is a review item for a human. Put "every operation declares 429 with Retry-After" on your review list, next to the checks in the API quality checklist.

What clients should do with these signals

A well-behaved client

  1. Read RateLimit on every response

    When r gets low, spread the remaining requests over the t seconds left instead of spending them at once.

  2. On 429, wait for Retry-After

    Retry-After wins over any other signal. If it is missing, back off exponentially with random jitter so a fleet of clients does not retry in lockstep.

  3. Retry only what is safe to repeat

    GET, PUT and DELETE are idempotent by definition. Retry a POST only if the API supports an idempotency key, or you risk creating the same order twice.

  4. Cap the retries

    Give up after a fixed number of attempts and surface the error. An infinite retry loop against a 429 is how a client turns a rate limit into an outage.

Check your limits from the browser

Load your spec in the API testing workbench, send a request to a limited operation, and open the response Headers tab to confirm RateLimit, RateLimit-Policy and Retry-After arrive with the values you documented. The workbench sends single requests, so it is for checking headers and the 429 body, not for load testing. One caveat: when CORS blocks the browser request and APIForge falls back to its server proxy, your API sees the proxy's IP address, so an IP-keyed limit counts those requests against the proxy rather than your machine. Test with an API key if your limits are keyed on credentials.

Frequently asked questions

Return 429 Too Many Requests, defined in RFC 6585. Include a Retry-After header with the number of seconds to wait and a body that explains which limit was hit. Use 503 only when the whole service is overloaded.

They solve different problems. Token bucket allows bursts up to its capacity while holding an average rate, which suits interactive clients. Sliding window counter enforces a steadier per-window number with two counters per client.

Not yet. draft-ietf-httpapi-ratelimit-headers is an active Internet-Draft in the IETF HTTPAPI working group, and field names changed between versions. Document the exact fields you send, and keep Retry-After on 429 responses because RFC 9110 defines it today.

Sources

  1. RFC 6585: Additional HTTP Status Codes, section 4 (429 Too Many Requests)IETF
  2. draft-ietf-httpapi-ratelimit-headers: RateLimit header fields for HTTPIETF HTTPAPI Working Group
  3. RFC 9110: HTTP Semantics, Retry-AfterIETF
  4. OWASP API4:2023 Unrestricted Resource ConsumptionOWASP