Appropriate way to make multiple client side (web) Google API calls with JavaScript/fetch and handle 429 errors

I need to write a pure client side JS function that will take in a potentially very large queue of Google API specs (URL, headers, body, content type, etc.) and fetch them. It will fetch 4 concurrently at a time. The issue is that I know some requests may hit the 429 error. I am trying to figure out how best to architect my code to handle this.

How it will work:

  • Each request in the queue is one of two types:
    • solo requests are just single/normal requests
    • batchable requests are requests that can be multipart batched with other requests in the queue for the same endpoint
  • Both solo responses, and the individual responses in a batch request may have a nextPageToken. This will result in a new solo or batchable request being added back to the queue.
  • There are 3 different 429 possible situations:
    1. solo request could get a 429 response
    2. main batch request could get a 429 response
    3. 1 or more individual items in a batch request could get a 429 response

This is what I have so far:

Start Workers

  1. Take the first item from the queue
    • If it is a solo request, call an async function doIt to process it
    • If it is a batchable request, look for the next up to 50 requests in the queue that are for the same API endpoint and create a single batch request and send that single batch request to doIt
  2. Repeat #1 until we have maxConcurrentWorkers (4) concurrent executions/workers of doIt or there are no more items in the queue.

doIt

  1. Check if we need to wait; if so async wait/sleep (using Promise and setTimeout)
  2. Make an await fetch call
  3. Check/process the response:
    • For solo requests:
      • If the response is good:
        1. Take the response and send it to a function to be processed
        2. If there is a nextPageToken, add a new solo request to the queue
        3. Start the next worker to keep processing the queue
      • If the response is 429:
        1. Add the request back to the queue as a solo request
        2. Set a global variable that tells the other workers to wait x seconds (#1 step in doIt)
        3. Start the next worker to keep processing the queue
    • For batch requests:
      • If the response is 429 (meaning the entire batch request failed):
        1. Iterate over each individual request in the batch request and readd them back to the queue
        2. set a global variable that tells the other workers to wait x seconds
        3. Start the next worker to keep processing the queue
      • If the response is good, iterate over each individual response and check their HTTP status
        • If the individual batch item response is good:
          1. Take the response and send it to a function to be processed
          2. If there is a nextPageToken, add a new batchable request to the queue
          3. Start the next worker to keep processing the queue
        • If the individual batch item response is 429:
          1. Add that specific request back to the queue as a batchable request
          2. Set a global variable that tells the other workers to wait x seconds
          3. Start the next worker to keep processing the queue

Does this logic make sense? Does it look good? Are there any cases/situations I am not accounting for? If so, what are they and how would/should I account for them?

I need to write all the code for this. I cannot use any libraries, including Google JavaScript client libraries.

You’ve stated that you cannot use Google’s Javascript Client libraries.

How about analyzing the code in those libraries (they are open-source and available on Github) to see how they handle those scenarios and adapting them to your needs? That seems like a viable approach.

gapi does not handle nextPageToken or 429 errors. That has to be implemented by the client. So it’s the same/similar problem.

Pagination is something you’d have to handle yourself (via nextPageToken).

But IIRC the google-api node.js client uses the gaxios library under-the-hood, and that library does have a retry mechanism in place (with support for exponential back-off) to handle 429 (Too Many Requests) errors. Investigate the open-source code on github for that library and see what you can find.

That is a complex code base. It’ll take me a long time to go through it and figure it out. I’m hoping someone here already understands it and can summarize it for me.

A few thoughts on the architecture — the overall queue/worker model is sound, but there are a handful of edge cases worth tightening up.

**1. Respect the `Retry-After` header instead of a fixed `x` seconds.** Google APIs often return a `Retry-After` on 429s (and sometimes a `RetryInfo` in the error body). Read that and use it as your wait time; fall back to exponential backoff only when it’s absent. A blind fixed wait will either back off too little (you keep getting 429s) or too much (you stall unnecessarily).

**2. Use exponential backoff *with jitter*, per-request.** Your current design sets one global “everyone wait x seconds” flag. That’s fine as a global cooldown, but each individual request should also track its own attempt count so its backoff grows (1s, 2s, 4s, 8s…) and you add random jitter so retries don’t all fire at the same instant (the thundering-herd problem). Without jitter, your 4 workers will tend to resynchronize and hit the limit together again.

**3. Cap retries and surface permanent failures.** Right now a request that keeps 429ing gets re-added to the queue forever. Add a `maxRetries` (e.g. 5–8) and a terminal “failed” bucket so the caller knows what never succeeded, rather than looping indefinitely.

**4. Distinguish 429 from other retryable statuses.** 500/502/503 are also worth retrying with the same backoff; 4xx like 400/401/403 are *not* retryable and should fail fast. Branch on that — otherwise a bad request silently recirculates.

**5. The global-wait flag has a race.** Multiple workers can read/observe the flag at slightly different times and all reset it. Safer to track a single `nextAllowedTime` timestamp (Math.max of current and now+delay) that every worker checks before fetching, rather than a boolean + setTimeout per worker.

**6. Worker lifecycle.** Instead of “start the next worker” recursion (which can blow the stack and makes concurrency hard to reason about), consider N long-lived workers that each loop: pull from queue → process → repeat, until the queue is drained *and* no in-flight requests can still enqueue more (the nextPageToken case). You need a “pending work” counter, not just queue length, or workers may exit while a paginated follow-up is still coming.

**7. Batch 429 nuance.** When the *outer* multipart batch returns 429, re-queue the children as you described — but also count that as a backoff signal. When only *some* inner items are 429, re-queue just those, but be aware their nextPageToken progress must be preserved per-item so you don’t lose or duplicate pages.

Net: keep the queue + bounded concurrency, but swap the global boolean for a shared `nextAllowedTime`, do per-request backoff-with-jitter honoring `Retry-After`, add retry caps + a failure bucket, and track in-flight work so pagination doesn’t let workers exit early. That covers the cases I think you’re currently missing.