API Design · Lesson 24

API Rate Limit Headers Explained

Learn how API rate limit headers communicate quotas, remaining capacity, retry timing, and client behavior across current and legacy formats.

Rate limiting is enforced by the server, but clients also need enough information to avoid repeatedly crossing the limit.

Response headers can communicate the active quota policy, remaining capacity, and when a client should slow down. The difficult part is that deployed APIs use several incompatible header formats, while the IETF specification is still evolving.

Quick answer

As of July 2026, there is no final RFC that standardizes API rate limit response headers.

The active IETF work-in-progress defines two structured fields:

RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=12;t=18

This means the policy allows 100 quota units in a 60-second window, with 12 units currently available and an effective window of 18 seconds.

A throttled response should still use the standard HTTP status and may include Retry-After:

HTTP/1.1 429 Too Many Requests
Retry-After: 18
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=0;t=18

Treat RateLimit fields as hints, not guarantees. If Retry-After is present, it takes precedence. Continue supporting documented legacy headers when existing clients depend on them.

Read Rate Limiting in APIs Explained first for token buckets, fixed windows, sliding windows, and enforcement choices.

The standards status matters

The current source is the IETF RateLimit header fields draft. Draft version 11 was published on May 23, 2026 and is explicitly a work in progress. It may be replaced or changed before becoming an RFC.

That distinction affects production documentation. Do not write that RateLimit-Policy and RateLimit are universally standardized or that every HTTP client understands them. A safer statement is:

This API implements the field syntax from draft-ietf-httpapi-ratelimit-headers-11. Clients must tolerate fields being absent or malformed.

Earlier draft versions described separate fields such as RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Many blog posts and libraries still show that model. Version 11 instead uses RateLimit-Policy and RateLimit, each identifying a named policy with parameters.

Real APIs also expose vendor fields such as:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 12
X-RateLimit-Reset: 1784390460

Those names and reset semantics are conventions, not one interoperable contract. Reset might be an epoch timestamp, a duration, or a vendor-specific value. Clients must follow the provider’s documentation.

Read the current field model

RateLimit-Policy describes the policy. Its important parameters are:

ParameterMeaning
qQuota allocated by the policy
quOptional quota unit name; requests is the default
wOptional policy window in seconds
pkOptional partition key describing the quota partition

A simple request quota can be described as:

RateLimit-Policy: "search";q=120;w=60

RateLimit describes current availability under a named policy:

ParameterMeaning
rRemaining quota currently available
tEffective window in seconds
qOptional quota value when useful in the current state

Example:

RateLimit: "search";r=24;t=12

The policy name connects the two fields. It also prevents a client from incorrectly combining remaining quota from one policy with the time window of another.

Multiple policies may appear:

RateLimit-Policy: "hour";q=1000;w=3600, "day";q=5000;w=86400
RateLimit: "day";r=100;t=36000

The server is signaling that the daily policy is currently the most constraining one. A client should not assume it may send all 100 requests immediately. Network latency, concurrent workers, other limits, and dynamic server decisions can change the result.

RateLimit does not replace 429

Headers do not enforce a quota. The server still decides whether to serve a request.

When a client exceeds a request limit, 429 Too Many Requests is the usual status. RFC 6585 defines 429 and allows a Retry-After header that tells the client how long to wait.

A useful response combines HTTP semantics with a stable error body:

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 18
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=0;t=18

{
  "type": "https://api.example.com/problems/rate-limit-exceeded",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "Retry this request after 18 seconds.",
  "instance": "/problems/req_01JZ8Y"
}

The status code tells generic HTTP software what happened. Retry-After gives an explicit delay. The RateLimit fields describe quota state. The problem body gives application-level context.

Read Problem Details for HTTP APIs for the error format and REST API Error Handling Best Practices for stable error codes.

Retry-After wins

Retry-After is defined by HTTP and can contain either a delay in seconds or an HTTP date.

Delay form:

Retry-After: 30

Date form:

Retry-After: Sat, 18 Jul 2026 16:45:00 GMT

For API clients, the delay form is often easier because it avoids clock comparison. The client still needs to cap unreasonable values and account for cancellation.

When Retry-After and RateLimit are both present, the current draft says Retry-After takes precedence. A client should not choose a shorter delay from the RateLimit effective window.

A robust client decision is:

  1. If Retry-After is valid, use it.
  2. Otherwise, if a valid RateLimit field reports zero remaining, consider its effective window.
  3. Otherwise, apply the client’s normal retry and backoff policy.
  4. Cap the delay and add jitter so a large client fleet does not resume simultaneously.

Read Retry Patterns Explained before retrying writes, and use Idempotency in APIs Explained when a request may be repeated.

Spring Boot response example

The following filter demonstrates where header emission belongs. The quota decision comes from a rate-limit service; the filter only translates that decision into HTTP.

@Component
public final class RateLimitHeadersFilter extends OncePerRequestFilter {
    private final RateLimitService rateLimitService;

    public RateLimitHeadersFilter(RateLimitService rateLimitService) {
        this.rateLimitService = rateLimitService;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws ServletException, IOException {
        RateLimitDecision decision =
                rateLimitService.evaluate(request.getRemoteUser(), request.getRequestURI());

        response.setHeader(
                "RateLimit-Policy",
                "\"default\";q=" + decision.quota() + ";w=" + decision.policyWindowSeconds()
        );
        response.setHeader(
                "RateLimit",
                "\"default\";r=" + decision.remaining() + ";t=" + decision.effectiveWindowSeconds()
        );

        if (!decision.allowed()) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.setHeader("Retry-After", Long.toString(decision.effectiveWindowSeconds()));
            response.setContentType("application/problem+json");
            response.getWriter().write("""
                {
                  "type": "https://api.example.com/problems/rate-limit-exceeded",
                  "title": "Rate limit exceeded",
                  "status": 429
                }
                """);
            return;
        }

        chain.doFilter(request, response);
    }
}

Important production details are outside this snippet:

  • Identify callers using authenticated principals or API keys, not only IP addresses.
  • Make quota updates atomic across application instances.
  • Do not reveal another tenant’s policy or partition identifier.
  • Ensure gateway and application limits do not publish contradictory headers.
  • Keep header generation consistent on both successful and throttled responses.

If a gateway owns enforcement, prefer emitting the headers there. Duplicating enforcement in a Spring filter can create two counters and confusing client behavior.

Node.js and Express response example

Express middleware can follow the same separation. A quota store returns a decision; middleware maps it to the response.

function rateLimitHeaders(evaluateQuota) {
  return async function applyRateLimit(req, res, next) {
    try {
      const decision = await evaluateQuota({
        principal: req.user?.id ?? req.ip,
        route: req.route?.path ?? req.path
      });

      res.set(
        "RateLimit-Policy",
        "\"default\";q=" + decision.quota + ";w=" + decision.policyWindowSeconds
      );
      res.set(
        "RateLimit",
        "\"default\";r=" + decision.remaining + ";t=" + decision.effectiveWindowSeconds
      );

      if (!decision.allowed) {
        res.set("Retry-After", String(decision.effectiveWindowSeconds));
        return res.status(429).type("application/problem+json").json({
          type: "https://api.example.com/problems/rate-limit-exceeded",
          title: "Rate limit exceeded",
          status: 429,
          instance: "/problems/" + req.id
        });
      }

      next();
    } catch (error) {
      next(error);
    }
  };
}

Do not calculate a shared production quota in process memory when the service runs multiple Node.js instances. Use a gateway or an atomic shared store. Also consider whether rejected requests consume quota; that choice must be consistent with the published policy.

Client parsing and safety

A client must treat server-provided values as untrusted input.

Do not blindly sleep for any value the server returns. A malformed or malicious response could contain a huge delay. Do not launch a burst simply because remaining capacity is positive. Do not assume headers will appear on the next response.

A Node.js client can prioritize Retry-After and cap the result:

function retryDelayMs(response, maxDelayMs = 60_000) {
  const retryAfter = response.headers.get("retry-after");
  if (!retryAfter) return null;

  const seconds = Number(retryAfter);
  if (Number.isFinite(seconds) && seconds >= 0) {
    return Math.min(seconds * 1000, maxDelayMs);
  }

  const date = Date.parse(retryAfter);
  if (!Number.isNaN(date)) {
    return Math.min(Math.max(date - Date.now(), 0), maxDelayMs);
  }

  return null;
}

Parsing the structured RateLimit field correctly requires an RFC 8941 Structured Fields parser. Avoid splitting complex field values on commas and semicolons by hand. Quoted strings, lists, and parameters make naive parsing fragile.

Caching, browsers, and intermediaries

RateLimit values on cached responses can be stale. The current draft recommends ignoring them when a response came from cache with a positive current age.

Browser JavaScript also cannot read arbitrary response headers across origins unless CORS exposes them:

Access-Control-Expose-Headers: RateLimit, RateLimit-Policy, Retry-After

Add that only when browser clients need the values. Read CORS Explained for Backend Developers and CORS Preflight Explained before changing cross-origin policy.

Gateways and proxies may enforce stricter limits than the origin. A component that changes headers must understand the same policy semantics. An unrelated intermediary should not make a quota look more permissive.

Monitoring and contract tests

Header behavior needs tests at the HTTP boundary.

Test at least these scenarios:

  • A successful response includes internally consistent policy and availability fields.
  • A near-limit response reports reduced remaining capacity.
  • A throttled response returns 429, Retry-After, and a safe error body.
  • Concurrent requests cannot produce negative remaining values.
  • Separate tenants or API keys do not share quota accidentally.
  • Cached responses do not drive client throttling decisions.
  • Malformed client-visible values are ignored safely.
  • CORS exposes the headers only for approved origins.
  • Gateway and application responses do not publish different policies.

Monitor allowed requests, rejected requests, quota-store latency, decision failures, Retry-After values, and the number of clients repeatedly exceeding limits. Headers are useful only when the underlying enforcement and measurements are trustworthy.

Common mistakes

The first mistake is calling an Internet-Draft a final standard. Pin the documented draft version and plan for change.

The second mistake is publishing old three-field examples as if every provider uses the same semantics.

The third mistake is treating remaining quota as a reservation. Parallel clients may consume it before the next request arrives.

The fourth mistake is returning headers without a real atomic limiter behind them.

The fifth mistake is sending Retry-After and a shorter RateLimit effective window, then leaving clients to guess.

The sixth mistake is exposing account identifiers or sensitive partition keys in headers.

The seventh mistake is trusting server-provided delays without caps, cancellation, or jitter.

Practical recommendation

For a new API in 2026:

  • Enforce limits in one authoritative layer.
  • Return 429 and Retry-After when throttling.
  • If adopting the IETF draft, publish RateLimit-Policy and RateLimit using a real Structured Fields implementation.
  • Document the exact draft version and compatibility policy.
  • Keep legacy headers only when clients require them.
  • Treat quota fields as advisory client hints, not service guarantees.
  • Test concurrency, isolation, CORS, caching, and unreasonable values.
  • Monitor whether clients actually slow down.

This approach gives clients useful signals without promising more stability than the current standards status supports.

Read Rate Limiting in APIs Explained for enforcement algorithms, API Abuse Prevention Explained for defensive layers, and API Gateway Explained for centralized enforcement.

For failure behavior, read Problem Details for HTTP APIs, REST API Status Codes Explained, Retry Patterns Explained, and Idempotency in APIs Explained. Review the IETF RateLimit draft before implementing because its field syntax may change.

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. Which description best matches API Rate Limit Headers Explained?

2. Which statement matches the lesson's quick answer?