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
| Layer | Scope | Limit | Code when exceeded |
|---|---|---|---|
| Per-IP guard | Per source IP (IPv6: per /64 block) | 35,000 requests per minute | RATE_DDOS_EXCEEDED |
| Per-endpoint quota | Per organization and per endpoint | Specific to each endpoint (ping allows 20 per minute) | RATE_TPS_EXCEEDED |
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_KEYevery 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:
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=means 20 requests in a 60-second sliding window.60
On a 429 you also get Retry-After in seconds: wait at least that long before retrying.
Recommended strategy
- 01Throttle preemptively
Read
RateLimit-Remainingon every response. When it drops below 20 % ofRateLimit-Limit, slow down so you reach the next reset with room to spare. - 02Respect 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.
- 03Exponential 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 intentosasync function callWithBackoff(request, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(request)
if (res.status !== 429) return res
const wait = Number(res.headers.get('Retry-After') ?? 1)
let delay = Math.min(wait * 2 ** attempt, wait * 4)
delay += Math.random() * delay * 0.5
await new Promise((r) => setTimeout(r, delay * 1000))
}
throw new Error('rate limit: retries exhausted')
}If you write your own client, parsing the policy is straightforward:
// 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. tells you which one fired:
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.