// Apps Script

Run parallel requests with fetchAll, not async/await.

Apps Script's V8 runtime accepts async/await syntax but has no event loop, so awaited calls run sequentially. UrlFetchApp.fetchAll() is the only way to send multiple HTTP requests in parallel and actually cut wall-clock time.

I want to fetch data from multiple APIs at the same time in Apps Script so my script finishes faster.

The script

copy · paste · trigger
parallelFetch.gs
Apps Script
// fetchAll runs all requests in one round-trip; async/await does not
function fetchPricesParallel() {
  var symbols = ['AAPL', 'GOOG', 'MSFT'];
  var baseUrl = 'https://query1.finance.yahoo.com/v8/finance/chart/';

  var requests = symbols.map(function(sym) {
    return { url: baseUrl + sym, method: 'get', muteHttpExceptions: true };
  });

  var responses = UrlFetchApp.fetchAll(requests);

  responses.forEach(function(resp, i) {
    var data = JSON.parse(resp.getContentText());
    var price = data.chart.result[0].meta.regularMarketPrice;
    Logger.log(symbols[i] + ': ' + price);
  });
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why async/await changes nothing in Apps Script

Apps Script runs on V8, which is the same JavaScript engine Chrome uses, so the syntax is valid — async functions, await expressions, Promise chains all parse without errors. The trap is that V8 alone does not give you concurrency. Concurrency in Node.js comes from libuv's event loop dispatching I/O callbacks while your thread does other work. Apps Script has no equivalent mechanism. When execution hits an awaited UrlFetchApp.fetch() call, the runtime blocks the script thread until the response arrives, exactly as it would in a plain synchronous call. The await keyword resolves the Promise, but the HTTP round-trip itself is still serial.

The first time I ran a timing test on this I convinced myself the script was broken: an async rewrite of a five-endpoint loop finished in 4.8 seconds, same as the synchronous original. It wasn't broken. Every await was just stalling the one thread in order, one call at a time.

How fetchAll actually parallelizes the work

UrlFetchApp.fetchAll() accepts an array of request descriptor objects and dispatches all of them before blocking. Google's infrastructure fans the requests out concurrently and collects the responses. The returned array is index-aligned with the input, so responses[2] always corresponds to requests[2] regardless of which server responded first.

The request descriptor is the same shape used for the single-call UrlFetchApp.fetch() — url, method, headers, payload, muteHttpExceptions, and so on. One key habit: set muteHttpExceptions: true on each entry. Without it, a single non-2xx response throws and discards every other result you already have. Catch per-response errors by checking resp.getResponseCode() in the forEach loop instead.

Wall-clock savings scale linearly with the number of requests up to the point where the slowest responder becomes the bottleneck. Three 600ms APIs sequentially take roughly 1,800ms; fetchAll collapses that to roughly 600ms (plus a few milliseconds of overhead). The Apps Script execution-time limit is 6 minutes for personal accounts and 30 minutes for Workspace, so for scripts that hit 10-20 external endpoints this difference is often the margin between finishing and timing out.

Handling failures per response

Because fetchAll does not throw on individual HTTP errors when muteHttpExceptions is true, you need to inspect each response yourself. A clean pattern is to map the responses array into result objects immediately: check getResponseCode(), JSON.parse only on 200, and push an error sentinel for anything else. This keeps the success path clean and gives you a structured failure log rather than a partially-written sheet with no indication of what was skipped.

One gotcha with POST requests in a mixed batch: each descriptor that sends a payload needs its Content-Type header set explicitly. fetchAll does not infer content type from the payload the way some HTTP clients do. Missing that header on one entry in a ten-item batch is a confusing failure mode because nine requests succeed and one returns 415 with no obvious link to the descriptor shape.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Does async/await work at all in Apps Script?
The syntax is valid under V8 and the script will run without errors, but awaited calls execute sequentially. There is no event loop to dispatch concurrent I/O, so the async wrapper adds no performance benefit for UrlFetchApp calls. Use it only if you need Promise chaining for control flow, not for speed.
Is there a limit on how many requests fetchAll can handle at once?
The UrlFetchApp daily quota is 20,000 calls for personal Google accounts and 100,000 for Workspace, and each URL in a fetchAll batch counts as one call toward that quota. There is no documented per-batch cap on array length, but in practice batches above 100 entries can produce inconsistent results; break large jobs into chunks of 50-100 and call fetchAll once per chunk.
How do I send POST requests with a JSON body using fetchAll?
Set method: 'post', payload: JSON.stringify(yourObject), and headers: {'Content-Type': 'application/json'} inside each request descriptor. The payload must be a string, not a plain object — fetchAll does not serialize it automatically.
Can I use fetchAll inside a custom function called from a spreadsheet cell?
No. Custom functions (those called directly from a cell formula) cannot use UrlFetchApp at all due to the service whitelist for that context. Use a regular function triggered by a button, time-based trigger, or Apps Script editor run instead.