Build the mental model
You already know hitting a rate limit gets you a 429. What the prerequisite course didn't cover is that many APIs expose your current standing in response headers on every request.
- A limit — the maximum requests allowed in the current window.
- A remaining count — how many requests you have left right now.
- A reset time — when the window refreshes and remaining goes back up.
The strategic shift is combining that header information with retry and backoff knowledge. Read the remaining count before sending each request and slow down proactively before you hit zero — this turns rate limiting into a budget you manage.
When a 429 does arrive, many providers include a Retry-After header — respect it directly. If no such hint is given, fall back to exponential backoff with jitter, doubling the wait each time a retry also fails.
RATE LIMIT CHECK AND BACKOFF FLOW
---------------------------------
-----
Before each request: check remaining count
|
+----+----+
| |
remaining>0 remaining=0
| |
v v
Send request Wait (exponential backoff + jitter)
| |
v v
Update remaining Retry request
from response |
v
Update remaining, continueConnect it to a real scenario
The code below models a queue of four pending API calls against a rate limiter with only two requests remaining. processQueue checks the remaining count before sending each one.
The first two calls succeed normally, decrementing remaining. By the third call, remaining hits zero, so the function reports RATE_LIMITED and simulates the backoff wait.
Run it and look at the action field: two SENT, one that had to back off, and one more SENT afterward using the refreshed count.
Try the working example
function processQueue(queue, remainingStart, limit) {
let remaining = remainingStart;
const log = [];
queue.forEach((call) => {
if (remaining > 0) {
remaining--;
log.push({ call, action: "SENT", remainingAfter: remaining });
} else {
const backoffMs = 2000;
remaining = limit - 1;
log.push({ call, action: `RATE_LIMITED -> waited ${backoffMs}ms -> retried, SENT`, remainingAfter: remaining });
}
});
return log;
}
const queue = ["GET /users", "GET /orders", "GET /invoices", "GET /reports"];
console.log(processQueue(queue, 2, 5));[
{ call: 'GET /users', action: 'SENT', remainingAfter: 1 },
{ call: 'GET /orders', action: 'SENT', remainingAfter: 0 },
{
call: 'GET /invoices',
action: 'RATE_LIMITED -> waited 2000ms -> retried, SENT',
remainingAfter: 4
},
{ call: 'GET /reports', action: 'SENT', remainingAfter: 3 }
]5-minute try-it
Modify processQueue so it also reads a simulated Retry-After value and uses that exact wait time instead of the fixed backoffMs when rate-limited.
One important caution
Assuming every API uses the same header names for rate-limit info instead of checking that provider's documentation.
Retrying immediately in a tight loop after a 429 instead of respecting Retry-After or backing off exponentially.
MDN — 429 Too Many Requests — API Integration & Webhooks