Queues and fair rate limits for many tenants

Learn to queue bursty API work, size worker pools from rate limits and latency with Little's law, and share one plan's limits and credits fairly across tenants.

  • 40 min
  • Expert

APV-300 taught you to stay under a rate limit from one process: pace calls, honour retry-after, batch where it pays. This lesson starts where that stops. Your product has many customers. Some import 40,000 contacts at 09:00 on a Monday; most do nothing for days. All of them share one APIVerve account, which means one credit allowance and, unless you split it, one per-minute window. The architecture questions are now: where does a burst wait, how many workers should drain it, who goes first, and what does the system say when it is genuinely full?

Level the load with a queue

A synchronous design calls the API inside the user's request. That is right for small, interactive work where the user is waiting for the answer. It is wrong for anything a single user can make large, because the request rate your users generate is unrelated to the request rate your plan allows. The import of 40,000 contacts either runs at the API's pace (and the HTTP request times out) or runs at the user's pace (and hits 429s within seconds).

Queue-based load levelling separates the two rates. The request handler writes one job per unit of work to a durable queue and returns at once, typically 202 Accepted with a job id the client can poll or subscribe to. A pool of workers takes jobs from the queue and calls the API at a pace you control. The burst still arrives; it just waits in the queue instead of in your users' browsers or in the provider's rate limiter.

Three properties make a queue worth having rather than merely present:

  • Durability. Jobs survive a deploy or a crashed worker. A job is acknowledged only after its result is stored, so a worker that dies mid-job leaves it to be picked up again.
  • Visibility. You can see depth and, more usefully, the age of the oldest job. Depth tells you how much work is waiting; age tells you whether anyone is being failed.
  • Bounded size. A queue with no limit is not load levelling; it is a delay you have not measured yet. The section on backpressure covers what to do at the bound.

Because a job can run twice (a worker can crash after calling the API and before acknowledging), make each job safe to repeat. APIVerve's endpoints are lookups and transformations, so a repeat costs credits but changes nothing on the provider's side. Your own side effects are the risk: write results with an upsert keyed by the job id, not an insert.

Size the worker pool with Little's law

How many workers should drain the queue? Not "as many as possible": workers beyond what your rate limit allows only produce 429s. Not "one": a single worker spends most of its time waiting on network latency. The answer comes from Little's law, which holds for any stable system:

L = λ × W
in-flight requests = arrival rate × time each request spends in the system

Turn it around for a worker pool. If you want to sustain a throughput of λ requests per second and each call takes W seconds, you need L calls in flight, which means L workers (each worker holds one call at a time).

Work it through with real numbers. At the time of writing, APIVerve's published per-minute limits are 5 on Free, 60 on Starter and 180 on Pro, with no per-minute ceiling on Mega (check rate limits for the current table; the effective limit can also be lowered per key). Take a Pro account:

  • Target throughput: 180 per minute = 3 requests per second.
  • Typical latency of the call: 0.6 seconds. L = 3 × 0.6 = 1.8, so 2 workers keep the limit busy.
  • Latency on a bad day: 2.5 seconds. L = 3 × 2.5 = 7.5, so 8 workers are needed to hold the same throughput.

That gap is the point. Size the pool for the latency you will see under stress (a high percentile, not the median), and let a pacer be the thing that enforces the rate. On a good day, most of the 8 workers wait on the pacer; on a bad day, they are all busy and the rate still holds. Without a pacer, 8 workers on a good day would try to send 13 requests per second into a 3 per second limit.

The pacer should refill continuously rather than in one-minute chunks. APIVerve's limiter works the same way, so a steady stream spread across the minute never trips a ceiling that the same number of calls fired in one second usually does. A token bucket with a small burst allowance is the standard shape:

pacer.mjs
// A token bucket shared by every worker in the process.
export function pacer(perMinute, burst = Math.max(1, Math.round(perMinute / 20))) {
  const ratePerMs = perMinute / 60_000;
  let tokens = burst;
  let last = Date.now();
  let pausedUntil = 0;
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  return {
    async take() {
      for (;;) {
        const now = Date.now();
        if (now < pausedUntil) { await sleep(pausedUntil - now); continue; }
        tokens = Math.min(burst, tokens + (now - last) * ratePerMs);
        last = now;
        if (tokens >= 1) { tokens -= 1; return; }
        await sleep(Math.ceil((1 - tokens) / ratePerMs));
      }
    },
    // A rate-limit 429 pauses everyone, not just the worker that saw it.
    pause(seconds) { pausedUntil = Math.max(pausedUntil, Date.now() + seconds * 1000); tokens = 0; },
  };
}

Two operational rules go with it. Set the pacer a little under the plan limit (say 90%), because interactive traffic on the same key also counts. And when any worker gets a rate-limit 429, pause the whole pool for retry-after seconds. The edge holds a rate-limited key in a short cooldown of around 50 seconds, and a pool that keeps sending during it extends the lockout for everyone.

Little's law also gives you drain time. A backlog of 40,000 jobs at 3 per second takes about 3.7 hours, before any new work arrives. That is a number to put in front of the product team before the first big customer imports, not after.

Credits are often the tighter limit

Rate is per minute; credits are per month, and they vary by API. Email Validator costs 20 credits a call. Running it flat out on Pro is 180 × 20 = 3,600 credits a minute, which would spend Pro's 1,000,000 monthly credits in under five hours. For expensive APIs, the per-minute ceiling is not what bounds you; the monthly allowance is, and your scheduler must meter credits as well as calls.

Try it liveEmail Validator20 credits
GEThttps://api.apiverve.com/v1/emailvalidator
curl "https://api.apiverve.com/v1/emailvalidator?email=jane%40mailinator.com" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

isDisposable is true. Note the x-api-remaining-credits header drop: this API costs more than most, which is what your per-tenant ledger must count.

Check yourself

A worker pool calls an API at a paced 2 requests per second. Under load, each call takes 3 seconds. How many workers do you need to hold 2 requests per second, and what stops them exceeding it on a fast day?

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales