Development

What HTTP status codes should I use for API rate limiting

Learn proper HTTP status codes for rate limiting and throttling, including best practices for communicating limits to API clients.

By Inventive HQ Team

When an API client exceeds its rate limit, the correct response is 429 Too Many Requests (defined in RFC 6585), paired with a Retry-After header telling the client how long to wait. Use 503 Service Unavailable only when the server is overloaded — not when a single client is over its quota — and never use 403 Forbidden, which implies a permanent authorization failure rather than a temporary throttle. Alongside 429, most APIs also return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers (or the emerging IETF-standard RateLimit field) so clients can self-throttle before they ever hit the wall.

That's the summary an AI Overview will give you. Here's what it can't: the precise line between 429 and 503, the fact that Retry-After accepts both a seconds count and an HTTP date, which header conventions the big APIs actually ship, and a decision table you can implement straight against.

Client hitting an API rate limit and receiving 429 Too Many Requests with Retry-After Requests flow from a client to an API server, draining a rate-limit quota bar. When the remaining quota reaches zero, the server responds with HTTP 429 Too Many Requests and a Retry-After header. A request over the limit → 429 + Retry-After Client bursts requests API server rate limiter

X-RateLimit-Remaining

429 Too Many Requests Retry-After: 60 X-RateLimit-Remaining: 0

HTTP Status Codes for Rate Limiting

When API clients exceed rate limits, the server must communicate this clearly. HTTP provides specific status codes and headers for this purpose. Proper rate-limiting communication enables clients to:

  • Understand why requests were rejected
  • Determine when to retry
  • Adjust behavior to stay within limits
  • Build resilient systems

Primary Status Code: 429 Too Many Requests

HTTP 429: The Standard Response

Status: 429 Too Many Requests

Meaning: Client has sent too many requests in a given time window

RFC: Defined in RFC 6585 (Additional HTTP Status Codes)

When to use: Any time client exceeds rate limits

Example:

GET /api/search?q=query HTTP/1.1

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706779200

{
  "error": "Rate limit exceeded",
  "message": "You have exceeded 100 requests per minute",
  "retry_after": 60
}

Why 429 is Better Than Alternatives

Three status codes get confused for rate limiting. Only one is correct, and the difference is who is at fault and whether the condition is temporary:

Status codeWhat it actually meansFaultClient shouldUse for rate limiting?
429 Too Many RequestsThis client exceeded its request quotaClientWait Retry-After, then retryYes — canonical (RFC 6585)
503 Service UnavailableThe server is overloaded or down for everyoneServerBack off (honor Retry-After if sent)Only for server-wide overload, not per-client limits
403 ForbiddenClient is not authorized for this resourceClientDo not retry — access is deniedNo — implies permanent denial
202 AcceptedRequest accepted but deferred/queuedPoll job status or retry laterOnly when you queue instead of reject

The trap is 503: it looks reasonable because the client did get refused, but it signals a server problem, so clients often stop retrying for long periods and log it as an outage. 429 keeps the fault where it belongs — on the client's request rate — and pairs cleanly with Retry-After.

Wrong: Using 503 Service Unavailable

503 confuses clients into thinking SERVER is broken
Clients may stop retrying for long periods
Suggests widespread outage rather than client's fault

Wrong: Using 403 Forbidden

403 is for authorization/permission issues
Rate limiting is temporary, not permanent
Client might never retry

Correct: Using 429 Too Many Requests

Clearly indicates rate limiting, not server error
Client knows they're temporarily throttled
Client can intelligently retry after specified time
Doesn't suggest permanent access denial

Rate Limiting Response Headers

The status code tells the client what happened; the headers tell it how much room is left and when to come back. Here is the working set:

HeaderPurposeExample valueStandardized?
Retry-AfterHow long to wait before retrying60 (seconds) or Sun, 31 Dec 2025 23:59:59 GMT (HTTP date)Yes — RFC 9110
X-RateLimit-LimitMax requests allowed in the window100De facto convention
X-RateLimit-RemainingRequests left in the current window42De facto convention
X-RateLimit-ResetWhen the window resets (usually Unix time)1706779200De facto convention
RateLimit / RateLimit-PolicyStructured quota + policy in one fieldRateLimit: limit=100, remaining=42, reset=30IETF draft (not yet an RFC)

Retry-After is the only header here that is formally standardized (RFC 9110). The X-RateLimit-* trio is a widely copied convention, not a spec — which is why the exact names differ from API to API (see the examples below).

Essential Headers for Rate Limiting

Retry-After: When client should retry

Retry-After: 60
[Server will accept requests in 60 seconds]

or

Retry-After: Sun, 31 Dec 2025 23:59:59 GMT
[Server will accept requests after this timestamp]

X-RateLimit-Limit: Maximum requests allowed in time window

X-RateLimit-Limit: 100
[Client allowed 100 requests per minute/hour/etc]

X-RateLimit-Remaining: Requests remaining in current window

X-RateLimit-Remaining: 42
[Client has 42 requests left before hitting limit]

X-RateLimit-Reset: When the limit window resets (Unix timestamp)

X-RateLimit-Reset: 1706779200
[Limit resets at this Unix timestamp]

Complete Rate Limit Headers Example

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1706779200

[Response body]

When limit exceeded:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706779200
Retry-After: 45

{
  "error": "Rate limit exceeded"
}

Industry Standard Headers

GitHub API:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1372700873

Stripe API:

Retry-After: 2

Twitter API:

X-Rate-Limit-Limit: 15
X-Rate-Limit-Remaining: 14
X-Rate-Limit-Reset: 1420070400

AWS API:

x-amzn-RequestId: request-id
x-amzn-RateLimit-Limit: 3000

Notice the inconsistency: GitHub uses X-RateLimit-*, older Twitter/X used X-Rate-Limit-* (extra hyphen), and AWS prefixes with x-amzn-. Because none of these are standardized, clients have to special-case each API.

Advertisement

The IETF RateLimit Standard (In Progress)

The IETF is working to end that inconsistency with draft-ietf-httpapi-ratelimit-headers. It is still an Internet-Draft — not yet a published RFC — so don't assume clients understand it. Two things worth knowing:

  • Earlier drafts proposed three separate fields (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) that mirror the X-RateLimit-* convention without the vendor prefix.
  • The current draft consolidates these into a single structured RateLimit field plus a RateLimit-Policy field, e.g. RateLimit: limit=100, remaining=42, reset=30.

Providers such as GitLab and CircleCI already emit these fields. Until the draft is finalized, the pragmatic approach is to keep sending X-RateLimit-* and Retry-After, and optionally add RateLimit for forward compatibility.

Not sure what limits to advertise in the first place? Use the calculator below to translate a requests-per-window target into concrete per-second and burst numbers before you wire up the headers.

Loading interactive tool...

Rate Limiting Strategies and Status Codes

Strategy 1: Strict Rate Limiting (429 for Any Overage)

Approach: Reject any request exceeding limit

Window: 1 minute
Limit: 100 requests per minute

Request 1-100: Accept (200 OK)
Request 101: Reject (429 Too Many Requests)

When to use:

  • Public APIs needing strict control
  • Preventing abuse
  • Resource constraints
  • Fair use enforcement

Response:

429 Too Many Requests
Retry-After: 45
X-RateLimit-Remaining: 0

Strategy 2: Soft Limit (Warning Headers, No 429)

Approach: Allow overage but warn client with headers

Limit: 100 requests per minute
Soft threshold: 95 requests

Request 1-95: Accept (200 OK + warning headers)
Request 96-105: Accept (200 OK + urgent warning headers)
Request 106+: Reject (429 Too Many Requests)

Response for request 96:

200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 4
X-RateLimit-Warning: true
X-RateLimit-Warning-Threshold: 95

[Response body]

When to use:

  • Premium customers
  • Internal APIs
  • Trusted partners
  • Progressive degradation

Strategy 3: Graduated Rate Limiting (Progressive Delays)

Approach: Progressively increase response time as limit approached

Request 1-80: Normal response (0ms delay)
Request 81-95: Slight delay (100ms added)
Request 96-100: Moderate delay (500ms added)
Request 101+: Reject (429 Too Many Requests)

Response:

200 OK
X-RateLimit-Remaining: 5
[Response delayed by 500ms before sending]

When to use:

  • Natural traffic smoothing
  • Encouraging less aggressive clients
  • Protecting backend resources

Strategy 4: Queue-Based Rate Limiting (202 Accepted)

Approach: Accept requests above limit but queue them

Limit: 100 requests per second
Request 1-100: Process immediately (200 OK)
Request 101-200: Queue for later (202 Accepted)
Request 201+: Reject or queue (202 or 429)

Response for queued request:

202 Accepted
Location: /queue/jobs/abc-123
Retry-After: 30

{
  "status": "queued",
  "job_id": "abc-123",
  "queue_position": 45
}

When to use:

  • Batch processing APIs
  • Long-running operations
  • Fair resource allocation
  • User-facing APIs valuing reliability

Implementing Rate Limiting with HTTP Status Codes

Implementation Pattern

client → request → rate_limiter → decision

if request_count <= limit:
    ├─ response status: 200 OK
    ├─ add rate limit headers
    └─ process request

else if request_count > limit:
    ├─ if can_queue:
    │  └─ status: 202 Accepted (if async job queue)
    ├─ else:
    │  ├─ status: 429 Too Many Requests
    │  ├─ Retry-After header
    │  └─ rate limit headers
    └─ reject request

Code Example

from flask import request, jsonify

RATE_LIMIT_REQUESTS = 100
RATE_LIMIT_WINDOW = 60  # seconds

def check_rate_limit(client_id):
    """Check if client has exceeded rate limit"""
    key = f"rate_limit:{client_id}"
    current_count = redis.incr(key)

    if current_count == 1:
        # First request in window, set expiration
        redis.expire(key, RATE_LIMIT_WINDOW)

    ttl = redis.ttl(key)
    limit_reset = int(time.time()) + ttl

    return {
        "count": current_count,
        "limit": RATE_LIMIT_REQUESTS,
        "reset": limit_reset,
        "remaining": max(0, RATE_LIMIT_REQUESTS - current_count)
    }

@app.route("/api/data")
def get_data():
    client_id = get_client_id(request)
    rate_limit = check_rate_limit(client_id)

    # Add rate limit headers
    headers = {
        "X-RateLimit-Limit": str(rate_limit["limit"]),
        "X-RateLimit-Remaining": str(rate_limit["remaining"]),
        "X-RateLimit-Reset": str(rate_limit["reset"])
    }

    # Check if exceeded
    if rate_limit["count"] > RATE_LIMIT_REQUESTS:
        retry_after = rate_limit["reset"] - int(time.time())
        return jsonify({
            "error": "Rate limit exceeded",
            "retry_after": retry_after
        }), 429, {
            **headers,
            "Retry-After": str(retry_after)
        }

    # Request within limit
    return jsonify({"data": "value"}), 200, headers

Multiple Rate Limit Tiers

Tiered Rate Limiting

Many APIs use multiple limits (requests per second, minute, hour, day):

API Limits for user:
├─ Per second: 10 requests
├─ Per minute: 100 requests
├─ Per hour: 1000 requests
└─ Per day: 10,000 requests

Check in order (most restrictive first):
1. If per-second limit exceeded → 429
2. If per-minute limit exceeded → 429
3. If per-hour limit exceeded → 429
4. If per-day limit exceeded → 429
5. Otherwise → 200 OK

Response for Tiered Limits

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706779200
X-RateLimit-Limit-Type: minute

{
  "error": "Rate limit exceeded",
  "limit_type": "minute",
  "limit": 100,
  "retry_after": 45
}

Differentiated Rate Limits

Based on Client Type

Free tier:
├─ 10 requests per second
├─ 100 requests per minute
└─ 1000 requests per hour

Pro tier:
├─ 100 requests per second
├─ 1000 requests per minute
└─ 10,000 requests per hour

Enterprise tier:
├─ 1000 requests per second
├─ No minute limit
└─ No hour limit

Response varies:

Free tier exceeds limit:
429 Too Many Requests

Pro tier has higher limit:
Same API, but 429 only at higher threshold

Based on Resource Intensity

Simple queries: 1000 requests per minute
Complex queries: 10 requests per minute
Heavy computations: 1 request per minute

Rate limiting by resource cost:

Request /search?q=simple → Cost: 1 unit
Request /analytics/report → Cost: 100 units
Request /ml-model/predict → Cost: 500 units

Limit: 1000 units per minute

/search 1000 times: OK (1000 units)
/analytics 5 times + /search 500 times: OK (1000 units)

Client-Friendly Rate Limit Communication

Error Response with Guidance

{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "You have exceeded 100 requests per minute",
  "limit": {
    "requests": 100,
    "window": "minute"
  },
  "current": {
    "requests": 105,
    "window_reset": "2025-01-31T10:05:00Z"
  },
  "retry": {
    "after_seconds": 45,
    "after_time": "2025-01-31T10:04:45Z"
  },
  "upgrade": {
    "message": "Upgrade to Pro for higher limits",
    "url": "https://api.example.com/pricing"
  }
}

Helpful Response Codes

429 Too Many Requests        ← Standard rate limit
Retry-After: 60              ← When to retry
X-RateLimit-Limit: 100       ← Your limit
X-RateLimit-Remaining: 0     ← How many left
X-RateLimit-Reset: timestamp ← When limit resets

Testing Rate Limiting

Test Case Matrix

Test | Condition | Expected Status | Expected Headers
-----|-----------|-----------------|------------------
1    | Under limit | 200 OK        | Remaining > 0
2    | At limit   | 200 OK         | Remaining = 0
3    | Over limit | 429            | Retry-After set
4    | Window reset | 200 OK       | Remaining reset

Rate Limit Test Example

import time

def test_rate_limiting():
    client = APIClient()

    # Make requests up to limit
    for i in range(100):
        response = client.get("/api/data")
        assert response.status_code == 200
        assert int(response.headers["X-RateLimit-Remaining"]) == 99 - i

    # Next request exceeds limit
    response = client.get("/api/data")
    assert response.status_code == 429
    assert "Retry-After" in response.headers

    retry_after = int(response.headers["Retry-After"])
    assert retry_after > 0

    # Wait and retry
    time.sleep(retry_after + 1)
    response = client.get("/api/data")
    assert response.status_code == 200  # Should work now

Best Practices Summary

DO:

  • ✓ Use 429 for rate limit exceeded
  • ✓ Include Retry-After header
  • ✓ Provide X-RateLimit-* headers on all responses
  • ✓ Be generous with rate limits initially
  • ✓ Communicate limits clearly in documentation
  • ✓ Offer upgrade paths for higher limits
  • ✓ Document retry strategy

DON'T:

  • ✗ Use 503 for rate limiting (server not broken)
  • ✗ Use 403 (not permission-related)
  • ✗ Omit Retry-After header
  • ✗ Have unclear rate limiting rules
  • ✗ Lock out clients permanently
  • ✗ Change limits without notice
  • ✗ Implement silently dropping requests

Conclusion

Proper HTTP status codes and headers for rate limiting enable clients to gracefully handle throttling and adjust their behavior. Using 429 Too Many Requests with appropriate headers like Retry-After and X-RateLimit-* provides clear, actionable feedback that clients can respond to intelligently.

Well-implemented rate limiting communicates clearly, provides guidance on when to retry, and enables a positive experience even when requests are temporarily throttled. This benefits both API providers (protecting resources) and clients (knowing exactly how to behave).

Frequently Asked Questions

What HTTP status code should I use for rate limiting?

Use 429 Too Many Requests, defined in RFC 6585. It is the canonical code for a client that has sent too many requests in a given time window. Always pair it with a Retry-After header so the client knows how long to wait before trying again.

What is the difference between 429 and 503 for rate limiting?

429 Too Many Requests means a specific client has exceeded its own quota — the fault is on the client side and only that client is throttled. 503 Service Unavailable means the server itself is overloaded or down for everyone. Using 503 for per-client rate limiting misleads clients into thinking the whole service is broken and can cause them to back off far longer than needed.

Can Retry-After be a number of seconds or a date?

Both. The Retry-After header accepts either a non-negative integer number of seconds (for example, Retry-After: 60) or an HTTP-date (Retry-After: Sun, 31 Dec 2025 23:59:59 GMT). Seconds is the more common and simpler form for rate limiting.

What are the X-RateLimit-* headers?

They are de facto conventions used by many APIs to expose quota state: X-RateLimit-Limit (max requests in the window), X-RateLimit-Remaining (requests left), and X-RateLimit-Reset (when the window resets, usually a Unix timestamp). They are not part of any RFC, so naming varies between providers (GitHub uses X-RateLimit-, older Twitter used X-Rate-Limit-).

Is there an official standard for rate limit headers?

The IETF is standardizing them in draft-ietf-httpapi-ratelimit-headers, which is still an Internet-Draft, not yet a published RFC. Earlier drafts proposed separate RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset fields; the current draft consolidates these into a single structured RateLimit field plus a RateLimit-Policy field. Until it is finalized, the X-RateLimit-* convention remains the most widely deployed.

Why should I not use 403 Forbidden for rate limiting?

403 Forbidden signals an authorization problem — the client is not allowed to access the resource at all. Rate limiting is temporary, not a permission denial, so a client seeing 403 may assume the block is permanent and stop retrying entirely. 429 correctly communicates a temporary throttle the client can recover from.

Should I return rate limit headers on successful requests too?

Yes. Sending X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (or the RateLimit field) on every 200 OK response lets well-behaved clients slow down before they hit the limit, smoothing traffic and reducing the number of 429s you have to serve.

What status code should I use for queued or throttled-but-accepted requests?

If you accept a request but defer processing (for example into an async job queue) rather than rejecting it, 202 Accepted is appropriate, typically with a Location header pointing to the job status and a Retry-After hint. Reserve 429 for requests you are actually refusing.

API rate limitingthrottlingHTTP status codesAPI designtraffic management