GASのfetchAllで複数APIを
並列実行して処理を高速化する方法
100行ぶんのAI要約が終わらない——その原因は、たいてい「1件ずつ順番に待っている」ことです。GASのUrlFetchApp.fetchAll()(複数のHTTPリクエストをまとめて並列実行するメソッド)を使えば、待ち時間が重なり処理時間が大きく縮みます。基本の書き方から、Claude APIを並列で回す実装、1件ずつのエラー判定と再試行、件数の分割、6分制限との付き合い方まで動くコードで解説します。
Table of Contents
なぜ遅いのか|fetchは1件ずつ待っている
外部APIを呼ぶGASが遅いとき、CPUが忙しいわけではありません。ほとんどの時間は「相手の返事を待っている時間」です。UrlFetchApp.fetch()をループで回すと、1件目の応答を待ち切ってから2件目を投げます。1件2秒のAPIなら、100件で単純に200秒——これだけで6分の実行時間制限の半分以上を食います。
// 遅い書き方: 1件ずつ順番に待つ
function fetchOneByOne() {
const urls = [
"https://example.com/api/1",
"https://example.com/api/2",
"https://example.com/api/3",
];
const results = [];
urls.forEach((url) => {
const res = UrlFetchApp.fetch(url); // ここで応答を待ち切ってから次へ
results.push(res.getContentText());
});
return results; // 1件2秒なら3件で約6秒
}待ち時間が支配的なら、待ちを重ねればいい。それを実現するのがfetchAll()です。リクエストをまとめて渡すと、GASが並列に実行し、全部の応答が揃った時点で結果の配列を返します。
fetchAllが効く処理
AI要約・翻訳・分類の一括処理、複数サービスからのデータ取得、大量URLの生存確認。待ち時間が長く、件数が多い処理。
効かない・向かない処理
1件だけの呼び出し、前のレスポンスを次のリクエストに使う逐次処理、厳しいレート制限があるAPIへの連打。
基本の書き方|リクエストの配列を渡す
fetchAll()の引数は、リクエスト設定オブジェクトの配列です。設定の中身はfetch()の第2引数とほぼ同じで、そこにurlを含めるだけ。戻り値はHTTPResponseの配列で、渡した順番のまま返ってきます。
// 速い書き方: まとめて渡して並列に実行する
function fetchInParallel() {
const urls = [
"https://example.com/api/1",
"https://example.com/api/2",
"https://example.com/api/3",
];
// fetchAll にはリクエスト設定の配列を渡す
const requests = urls.map((url) => ({
url: url,
method: "get",
muteHttpExceptions: true, // 失敗しても例外を投げさせない
}));
const responses = UrlFetchApp.fetchAll(requests); // 3件が同時に走る
// レスポンスは渡した順番のまま返ってくる
return responses.map((res) => res.getContentText());
}大事なのがmuteHttpExceptions: true(HTTPエラーで例外を投げさせない設定)です。これを付けないと、30件のうち1件が500を返しただけで処理全体が例外で止まり、成功していた29件の結果もまとめて失われます。並列実行では必ず付けてください。
順番が保証される点も重要です。実行は同時でも、結果は渡した配列と同じ並びに揃えられます。だからresponses[i]はrequests[i]の結果——スプレッドシートの行との対応付けを、インデックスだけで安全に書けます。
AI要約を並列で回す|Claude APIの実装例
fetchAllが最も効くのがAI連携です。生成AIのAPIは1回の応答に数秒かかるため、逐次実行だと件数がそのまま待ち時間になります。ここではA列の問い合わせ文をClaude APIで要約し、B列に書き戻す例を示します。APIキーはPropertiesService(スクリプトに設定値を安全に保存する仕組み)から読み込みます。
// スプレッドシートのA列の問い合わせ文を、AIで一括要約してB列に書き戻す
const API_KEY = PropertiesService.getScriptProperties().getProperty("CLAUDE_API_KEY");
const MODEL = "claude-sonnet-5";
function summarizeAllInParallel() {
const sheet = SpreadsheetApp.getActiveSheet();
const lastRow = sheet.getLastRow();
if (lastRow < 2) return;
const rows = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
// 空行を除きつつ、元の行番号を覚えておく
const targets = [];
rows.forEach((row, i) => {
const text = String(row[0]).trim();
if (text !== "") targets.push({ row: i + 2, text: text });
});
if (targets.length === 0) return;
const requests = targets.map((t) => buildClaudeRequest(t.text));
const responses = UrlFetchApp.fetchAll(requests); // ここで一気に投げる
// レスポンスは requests と同じ順番。インデックスで元の行に対応付ける
responses.forEach((res, i) => {
const summary = readSummary(res);
sheet.getRange(targets[i].row, 2).setValue(summary);
});
}
function buildClaudeRequest(text) {
return {
url: "https://api.anthropic.com/v1/messages",
method: "post",
contentType: "application/json",
headers: {
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify({
model: MODEL,
max_tokens: 300,
messages: [
{ role: "user", content: "次の問い合わせを日本語1文で要約してください。\n\n" + text },
],
}),
muteHttpExceptions: true,
};
}
function readSummary(res) {
const code = res.getResponseCode();
if (code !== 200) return "エラー(" + code + ")";
const body = JSON.parse(res.getContentText());
return body.content[0].text; // テキストブロックの本文
}ポイントは、リクエストを組み立てる前に「対象の行番号」を配列で持っておくことです。空行を除くとインデックスがずれるため、{ row, text }の形で元の位置を覚えておけば、レスポンスを正しい行へ書き戻せます。
なお、書き戻しを1行ずつsetValueで行うと、そこが新たなボトルネックになります。行が連続している場合は結果をまとめてsetValuesで一括書き込みするほうが高速です。
1件ずつエラーを受け止める|失敗分だけ再試行
並列実行では「全部成功」か「全部失敗」ではなく、「30件中2件だけ失敗」が普通に起きます。特に多いのが429(レート制限超過。短時間に投げすぎ)です。並列で一気に投げるほど当たりやすくなります。
// 成功・失敗を1件ずつ判定し、失敗分だけ後で再実行できるようにする
function fetchWithResultCheck(requests) {
const responses = UrlFetchApp.fetchAll(requests);
const succeeded = [];
const failed = [];
responses.forEach((res, i) => {
const code = res.getResponseCode();
if (code === 200) {
succeeded.push({ index: i, body: res.getContentText() });
return;
}
// 429(レート超過)と5xx(サーバー側の一時障害)は再試行の価値がある
const retryable = code === 429 || code >= 500;
failed.push({ index: i, code: code, retryable: retryable });
console.warn("失敗 index=" + i + " code=" + code + " body=" + res.getContentText());
});
return { succeeded: succeeded, failed: failed };
}
// 失敗したぶんだけ、少し待ってからもう一度まとめて投げる
function fetchWithRetry(requests, maxRetry) {
let pending = requests.map((req, i) => ({ index: i, request: req }));
const results = [];
for (let attempt = 0; attempt <= maxRetry && pending.length > 0; attempt++) {
if (attempt > 0) Utilities.sleep(2000 * attempt); // 2秒 → 4秒 と待ちを伸ばす
const responses = UrlFetchApp.fetchAll(pending.map((p) => p.request));
const next = [];
responses.forEach((res, i) => {
const target = pending[i];
const code = res.getResponseCode();
if (code === 200) {
results.push({ index: target.index, body: res.getContentText() });
} else if (code === 429 || code >= 500) {
next.push(target); // 再試行のキューに戻す
} else {
results.push({ index: target.index, body: null }); // 400系は再試行しない
}
});
pending = next;
}
return results.sort((a, b) => a.index - b.index); // 元の順番に戻す
}判定の基準はシンプルです。429と5xxは「時間をおけば直る可能性がある」ので再試行、400や401はリクエスト側の問題なので何度投げても同じ、再試行しません。再試行では待ち時間を少しずつ伸ばす(2秒→4秒)と、相手のAPIにも優しく、成功率も上がります。
件数が多いときはチャンクに分ける
500件を一度にfetchAll()へ渡す——これは避けてください。公式に「何件まで」と明示された上限はありませんが、件数が増えるほどタイムアウトやレート制限のエラーが起きやすくなります。実務では20〜50件ずつのかたまり(チャンク)に分けるのが安全です。
// 件数が多いときは、かたまり(チャンク)に分けて呼ぶ
function fetchAllInChunks(requests, chunkSize) {
const size = chunkSize || 30; // 実務では20〜50件が扱いやすい
const all = [];
for (let i = 0; i < requests.length; i += size) {
const chunk = requests.slice(i, i + size);
const responses = UrlFetchApp.fetchAll(chunk);
responses.forEach((res) => all.push(res));
console.log((i + chunk.length) + " / " + requests.length + " 件完了");
Utilities.sleep(500); // APIのレート制限に配慮して少し間を空ける
}
return all; // 全チャンクの結果を、元の順番のまま連結して返す
}チャンクに分けると、進捗をログに出せる利点もあります。途中で止まったときに「どこまで終わったか」が分かり、原因の切り分けが早くなります。
割り当て(quota)の消費に注意
fetchAllを使っても、1日のURL Fetch呼び出し回数の割り当ては「リクエスト1本ずつ」でカウントされます。速くはなりますが、回数が節約できるわけではありません。同じデータを何度も取りに行っている場合は、CacheServiceで結果を一時保存し、そもそもの呼び出し回数を減らすほうが効きます。
6分制限との付き合い方|続きから再開する
fetchAllで速くなっても、GASの実行時間制限(無料アカウントで約6分)そのものは変わりません。数千件を扱うなら、時間内に終わらせようとせず「途中で切り上げて、続きから再開する」設計にします。処理済みの位置をPropertiesServiceに保存し、時間主導トリガーで自分自身を呼び直す形です。
// 6分に収まらないときは、処理位置を保存して続きから再開する
const PROP_KEY = "FETCH_CURSOR";
const TIME_LIMIT_MS = 4.5 * 60 * 1000; // 6分の手前で切り上げる
function processWithResume() {
const started = new Date().getTime();
const props = PropertiesService.getScriptProperties();
const sheet = SpreadsheetApp.getActiveSheet();
let cursor = Number(props.getProperty(PROP_KEY) || 2); // 2行目から開始
const lastRow = sheet.getLastRow();
while (cursor <= lastRow) {
if (new Date().getTime() - started > TIME_LIMIT_MS) {
props.setProperty(PROP_KEY, String(cursor)); // 次回の再開位置を保存
createResumeTrigger();
return;
}
const size = Math.min(30, lastRow - cursor + 1);
const rows = sheet.getRange(cursor, 1, size, 1).getValues();
const requests = rows.map((row) => buildClaudeRequest(String(row[0])));
const responses = UrlFetchApp.fetchAll(requests);
const out = responses.map((res) => [readSummary(res)]);
sheet.getRange(cursor, 2, out.length, 1).setValues(out);
SpreadsheetApp.flush(); // 途中結果を確実にシートへ反映する
cursor += size;
}
props.deleteProperty(PROP_KEY); // 最後まで終わったらリセット
}
function createResumeTrigger() {
ScriptApp.newTrigger("processWithResume")
.timeBased()
.after(60 * 1000) // 1分後に続きから再開
.create();
}制限の6分ぎりぎりまで粘らず、4分30秒あたりで切り上げるのがコツです。最後のチャンクが想定より長引いても、保存処理まで確実に到達できます。SpreadsheetApp.flush()でチャンクごとに書き込みを確定させておくと、強制終了しても途中結果が残ります。
なお、この例では再開のたびにトリガーが増えていくため、実運用では処理の冒頭で不要になった同名トリガーを削除する処理も入れてください。
まとめ
UrlFetchApp.fetchAll()は、外部API連携が遅いGASにいちばん効く一手です。ループのfetchをリクエスト配列に置き換えるだけで、待ち時間が重なり処理時間が縮みます。レスポンスは渡した順番のまま返るため、スプレッドシートの行との対応付けもインデックスで安全に書けます。
実務で押さえるべきは4点です。(1)muteHttpExceptions: trueを必ず付け、1件の失敗で全体を落とさない。(2)429と5xxだけを再試行し、待ち時間を伸ばす。(3)20〜50件のチャンクに分ける。(4)6分制限は続きから再開する設計で越える。AI要約や一括翻訳のように「1件数秒×大量件数」の処理ほど、効果がはっきり出ます。
よくある質問
複数のHTTPリクエストをまとめて渡し、並列に実行するGASのメソッドです。リクエスト設定の配列を渡すと、レスポンスの配列が同じ順番で返ってきます。1件ずつUrlFetchApp.fetchをforループで回すより待ち時間が重ならないため、件数が多いほど処理時間が短くなります。
APIの応答時間と件数によりますが、待ち時間が支配的な処理ほど効果が出ます。1件2秒かかるAPIを30件、fetchで順番に呼ぶと約60秒かかります。fetchAllならリクエストが同時に走るため、体感では数秒〜十数秒に縮むことが多いです。ただし並列数はGAS側の判断で決まるため、件数に比例して速くなるとは限りません。
各リクエストに muteHttpExceptions: true を付けていれば止まりません。HTTPエラーでも例外を投げずレスポンスが返るため、getResponseCode()で1件ずつ成功・失敗を判定できます。逆にこの指定がないと、1件の404や500で処理全体が例外になり、成功したぶんの結果も失われます。
公式に明示された上限件数はありませんが、数百件を一度に渡すとタイムアウトやエラーが起きやすくなります。実務では20〜50件ずつのかたまり(チャンク)に分けて呼ぶのが安全です。また1日のURL Fetch呼び出し回数の割り当ては消費されるため、fetchAllでもリクエスト1本ずつがカウントされます。
適用されます。fetchAllは待ち時間を重ねられるだけで、実行時間の上限そのものは変わりません(無料アカウントは約6分)。件数が多い場合は、処理済みの位置をPropertiesServiceに保存し、時間主導トリガーで続きから再開する設計にします。
返ってきます。実行は並列でも、戻り値の配列は渡したリクエスト配列と同じ順番に揃えられます。そのためインデックス(i番目のレスポンスはi番目のリクエストの結果)で元データと突き合わせできます。順番が保証されるので、スプレッドシートの行と対応付ける処理も安全に書けます。