Blog/Product

Rate limits that tell you what is happening

Every maviapi response carries its own limit headers, so a polite client never has to guess. Here is what they mean and how to build a backoff that reads them.

Musab Gültekin
Founder & CEO · 22 Apr 2026 · 6 min read
Product

Rate limits that tell you what is happening

A rate limit you can’t see is just a trap with a timer. So every maviapi response carries the numbers a polite client needs to never hit one: how much you’ve used, how much is left, and when it resets. Here’s how to read them and build a client that backs off on its own.

The guessing game

Without limit headers, throttling is folklore. You sprinkle in a sleep that felt about right, watch for 429s in the logs, and tune by superstition. It works until traffic shifts, and then you’re either leaving throughput on the table or hammering the limit and getting rejected. Either way you’re guessing, and the server knows the real answer it just isn’t telling you.

We decided to just tell you.

The headers we send

Every successful response includes the current state of your window:

response headers
x-ratelimit-limit: 60          # requests allowed per window
x-ratelimit-remaining: 57      # requests left in this window
x-ratelimit-reset: 1718960400  # unix time when the window resets
x-maviapi-version: 4

When you do run out, the rejection is just as informative. A 429 comes back with a retry-after telling you, in seconds, exactly how long to wait before trying again. No guessing, no exponential ramp into a wall.

when you are over the limit
HTTP/2 429
retry-after: 8
x-ratelimit-remaining: 0

Reading them in practice

The headers are plain strings on the response, so reading them is unremarkable. The point is to actually look before you fire the next request, not after you’ve already been rejected:

check before you spend
const res = await fetch(url, { headers: auth });

const remaining = Number(res.headers.get("x-ratelimit-remaining"));
const reset = Number(res.headers.get("x-ratelimit-reset"));

if (remaining <= 1) {
  const waitMs = Math.max(0, reset * 1000 - Date.now());
  console.log(`Window almost empty, pausing ${Math.ceil(waitMs / 1000)}s`);
  await sleep(waitMs);
}

A backoff that respects them

Tie it together and you get a request helper that never has to be rescued. When the server says wait, it waits the amount the server asked for, then carries on. No magic constants, no tuning:

polite-fetch.mjs
async function politeFetch(url, init, tries = 4) {
  for (let attempt = 0; attempt < tries; attempt++) {
    const res = await fetch(url, init);

    if (res.status !== 429) return res;

    // The server told us exactly how long to wait. Believe it.
    const retryAfter = Number(res.headers.get("retry-after")) || 1;
    await sleep(retryAfter * 1000);
  }
  throw new Error("Rate limited after several retries");
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

Four lines of real logic. Because the wait time comes from the response, this is correct whether your plan allows sixty requests a minute or sixty thousand. You’re not modeling our limits in your code; you’re reading them.

Limits, credits, and 429s

One thing worth keeping straight: rate limits and credits are different meters. A rate limit protects the service from bursts in the moment. A credit is your plan’s monthly budget, and one successful request to a data endpoint spends one of them.

  • A 429 means “too fast, slow down,” and a credit isn’t spent. Wait the retry-after and you’re fine.
  • Running out of credits is a billing state, not a timing one. No amount of backing off fixes it; that’s an upgrade or the start of the next period.

A request that fails validation or errors out doesn’t cost you a credit. You’re billed for served data, not for attempts, so a flaky source site never quietly eats your allowance.

Build the polite client once, drop it in front of every call, and rate limits stop being something you think about. The headers were there the whole time; all your code has to do is read them.

Written by
Musab Gültekin

Started maviapi after one too many 2am scraper fires. Writes about the product, the bets behind it, and the parts of running an API company nobody warns you about.

More from Musab

Keep reading

All posts →

New posts, no inbox required

We publish when we have something worth saying: new APIs, product updates, and engineering notes. Follow along however you like.