Skip to content

Usage limits and 429 Too Many Requests

Two layers protect the API: a per-source-IP guard in front of every request, and a per-organization, per-endpoint quota. Both expose their state in standard headers: read them and your client paces itself.

6 min readReviewed on

The two-layer model

The per-IP guard runs first: it is a safety floor, not a commercial commitment. Hitting it usually means a misconfigured shared IP or a clear abuse pattern; the threshold is generous and legitimate traffic does not get close. The per-endpoint quota is the one you will see in normal operation, and each endpoint states its own in the reference.

The two layers count differently: the IP layer adds up every request coming from the same IP, no matter the endpoint; the quota layer adds up requests to a single endpoint. Rotating endpoints does not multiply the IP budget, and rotating IPs does not multiply the endpoint quota.

IPv6: the /64 block is counted

With IPv6, the per-IP guard does not count the exact address but its /64 block. Every IPv6 connection is assigned a whole /64, so whoever uses it can pick a fresh address on every request without changing networks: counting the exact address would set no limit at all. Every address in the same /64 shares the 35,000 per minute. IPv4 addresses are counted one by one, as always.

The cut-off for API keys that do not exist

The per-IP guard has a second rule, for whoever is guessing keys. If 60 different API keys that do not authenticate arrive within one minute from the same IP (or the same /64) (keys that do not exist, or were revoked or expired), that IP is cut off for 15 minutes: every request gets 429 RATE_DDOS_EXCEEDED with a Retry-After saying how much of the cut-off is left, even if the key you send is valid.

  • It counts distinct keys, not attempts: an integration that keeps sending the same revoked key gets 401 AUTH_INVALID_KEY every time, but never reaches the cut-off.
  • Only keys that do not authenticate count. A valid key without permission for the service, from a suspended organization or used from a network it does not allow does not add up: it exists, and it cannot be guessed at random.
  • If your integration spreads traffic across several keys, check that none is miscopied or revoked: behind the same internet egress, they all count together.

Sliding window, not fixed buckets

Both layers use a two-bucket weighted sliding window, the same technique Cloudflare and Stripe use. The intuition:

  • The current 60-second bucket counts at full weight.
  • The previous bucket counts with a weight that decays from 1.0 (start of the current bucket) to 0.0 (end of it).
  • The effective total is current + previous × weight.

With fixed buckets, a client could send the full limit at second 59 and the same again at second 0 of the next bucket: double the limit in two seconds. The sliding window catches that.

Response headers

The RateLimit-* headers follow the IETF’s public proposal on usage-limit headers. Every response from an endpoint with its own limit carries:

HTTP
RateLimit-Limit:     20
RateLimit-Remaining: 18
RateLimit-Reset:     31
RateLimit-Policy:    20;w=60;name="endpoint"
RateLimit-Limit
Quota for the active window.
RateLimit-Remaining
Requests remaining in the active window.
RateLimit-Reset
Seconds until the active window resets.
RateLimit-Policy
Declarative quota in a parseable format. 20;w=60 means 20 requests in a 60-second sliding window.

On a 429 you also get Retry-After in seconds: wait at least that long before retrying.

Recommended strategy

  1. 01
    Throttle preemptively

    Read RateLimit-Remaining on every response. When it drops below 20 % of RateLimit-Limit, slow down so you reach the next reset with room to spare.

  2. 02
    Respect Retry-After

    When you get a 429, wait at least the seconds it reports before the next attempt. Retrying immediately just re-triggers the counter.

  3. 03
    Exponential backoff with jitter

    If retries keep failing, add an exponential delay (for example 2^n × 100 ms) with ±50 % random jitter. The jitter keeps thousands of clients from syncing on the same second.

import time, random

def call_with_backoff(client, request, max_retries=5):
    for attempt in range(max_retries):
        r = client.send(request)
        if r.status_code != 429:
            return r
        wait = int(r.headers.get("Retry-After", "1"))
        # retroceso exponencial con techo en Retry-After * 4, mas variacion
        delay = min(wait * (2 ** attempt), wait * 4)
        delay += random.uniform(0, delay * 0.5)
        time.sleep(delay)
    return r  # quien llama decide que hacer al agotar los intentos

If you write your own client, parsing the policy is straightforward:

JavaScript
// RateLimit-Policy: 20;w=60;name="endpoint"
// -> limite 20, ventana 60 s, nombre "endpoint"
const [limit, ...params] = policy.split(';')
const window = Number(params.find((p) => p.startsWith('w='))?.slice(2))

What you see when it fires

Both layers return HTTP 429 with the standard envelope. The error.code tells you which one fired:

429 Too Many Requests
RateLimit-Limit:     20
RateLimit-Remaining: 0
RateLimit-Reset:     42
Retry-After:         42

{
  "success": false,
  "error": {
    "code": "RATE_TPS_EXCEEDED",
    "type": "https://developers.hablame.co/docs/v6/errors/rate-tps-exceeded",
    "message": "You have exceeded the allowed request rate for this endpoint. Please wait before retrying.",
    "details": []
  },
  "meta": { "requestId": "c7d41a09e2b8f356" }
}

If you need more

If your traffic pattern legitimately exceeds the limit of an endpoint (bulk imports, campaigns, batch onboarding), tell us about your case at comercial@hablame.co. Have this ready:

  • The endpoint and the expected peak, along with the total daily volume.
  • Whether the peak is one-off (a launch) or sustained.
  • How you retry when you are limited, so we know that a higher limit does not just move the problem.

Need help?

Was this page helpful?

See all support channels