GAS『Service Timeout』の原因と対処法
- 作成日 2025.10.15
- その他
Google Apps Script実行中に「Service Timeout」が出るのは、外部サービスやApps Scriptの各サービス呼び出しが規定時間内に応答しなかったため。主因はUrlFetchAppの遅延、Spreadsheet/Drive操作の過多、同時実行の競合、ネットワーク一時障害など。処理の分割・同時化・一括化・再試行・締切管理で回避する。
目次
- 1. エラーの意味と発生条件
- 2. まず計測:どこで待っているかを特定
- 3. UrlFetchAppを高速化:並列化・タイムアウト・再試行
- 4. Spreadsheet操作の待ちを削減:一括読み書き+分割
- 5. 締切管理:自発停止→次回続き(Service Timeoutの回避線)
- 6. 同時実行の競合を避ける(LockService)
- 7. 短時間トリガーでは重処理を行わない(キュー→バッチ)
- 8. Advanced Sheets APIでバッチ更新(呼び出し回数の圧縮)
- 9. 外部API側の制約・遅延に備える(設計の指針)
- 10. よくあるNG→OK(クイック修正)
- 11. 再現→解消の通し例(外部API+シート書き込み)
- 12. チェックリスト(上から順に)
エラーの意味と発生条件
・UrlFetchApp / JDBC / Gmail / Calendar / Drive / Sheets などのサービス呼び出しが規定時間で返らない
・多数のAPI呼び出しを直列実行して待ち時間が累積
・スプレッドシートへの逐次アクセスで内部待ち(保護・競合・大量更新)
・同時実行や編集中の衝突で応答が遅延
・一時的なレート制限/内部エラーに伴う遅延まず計測:どこで待っているかを特定
// 区間計測のミニユーティリティ
function timed(label, fn) {
const t0 = Date.now();
const out = fn();
console.log(`⏱ ${label}: ${Date.now() - t0}ms`);
return out;
}
// 使用例
function probe() {
timed('openById', () => SpreadsheetApp.openById('YOUR_SHEET_ID'));
timed('fetch', () => UrlFetchApp.fetch('https://example.com'));
}・遅延区間が特定できれば、対処が明確になる。
UrlFetchAppを高速化:並列化・タイムアウト・再試行
// 1) 複数URLは fetchAll で並列
function fetchParallel(urls) {
const reqs = urls.map(u => ({
url: u,
followRedirects: true,
muteHttpExceptions: true,
// タイムアウト指定(ms):長すぎる待ちを避ける
timeout: 15000
}));
const res = UrlFetchApp.fetchAll(reqs);
return res.map(r => ({ code: r.getResponseCode(), body: r.getContentText() }));
}
// 2) 再試行(指数バックオフ+ジッター)
function withRetry(fn, tries = 5) {
let wait = 400;
for (let i = 0; i < tries; i++) {
try { return fn(); }
catch (e) {
const msg = e.message || '';
const retryable = /timeout|timed out|Rate limit|quota|Internal|Backend/i.test(msg);
if (!retryable || i === tries - 1) throw e;
Utilities.sleep(wait + Math.floor(Math.random() * 200));
wait = Math.min(wait * 2, 8000);
}
}
}・直列fetchの積み上げはTimeoutの温床。fetchAllと短めtimeout+再試行で安定させる。
Spreadsheet操作の待ちを削減:一括読み書き+分割
// NG:逐次 setValue(内部待機が蓄積)
for (let r = 0; r < data.length; r++) {
for (let c = 0; c < data[0].length; c++) {
sh.getRange(start + r, 1 + c).setValue(data[r][c]);
}
}
// OK:まとめて setValues
const range = sh.getRange(start, 1, data.length, data[0].length);
range.setValues(data);
// それでも大きいときはチャンク分割
function writeChunked(sh, start, values) {
const MAX_CELLS = 50000; // 目安
let i = 0;
while (i < values.length) {
const rows = Math.max(1, Math.min(values.length - i, Math.floor(MAX_CELLS / values[0].length)));
const block = values.slice(i, i + rows);
sh.getRange(start + i, 1, block.length, block[0].length).setValues(block);
i += rows;
Utilities.sleep(150); // 微スロットリング
}
}・往復回数とロック滞留を減らす。一気に書けない量は安全に分割。
締切管理:自発停止→次回続き(Service Timeoutの回避線)
function processWithDeadline() {
const props = PropertiesService.getScriptProperties();
const cursor = Number(props.getProperty('row') || 2);
const DEADLINE = Date.now() + 55_000; // 実行上限より手前で終了
const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
const sh = ss.getSheetByName('Data');
const last = sh.getLastRow();
let row = cursor;
while (row <= last) {
// ... 行処理 ...
row++;
if (Date.now() > DEADLINE) {
props.setProperty('row', String(row));
scheduleNext(1); // 1分後に再開
return;
}
}
props.deleteProperty('row'); // 完了
}
function scheduleNext(minutes) {
ScriptApp.newTrigger('processWithDeadline')
.timeBased().after(minutes * 60_000).create();
}・待ちが発生しそうな長処理は、上限前に自発終了→トリガーで継続。
同時実行の競合を避ける(LockService)
function job() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(30000)) return; // 先行実行に譲る
try {
// 競合しやすい範囲の更新
} finally {
lock.releaseLock();
}
}・ロック無しの並行更新は内部待ちを誘発し、Timeoutの引き金になる。
短時間トリガーでは重処理を行わない(キュー→バッチ)
// onEdit 内は軽い検証だけにしてキューへ積む
function onEdit(e) {
if (!e || !e.range) return;
const payload = { row: e.range.getRow(), time: Date.now() };
const cache = CacheService.getScriptCache();
cache.put('job', JSON.stringify(payload), 60); // 60秒保持
// 実作業は時間トリガー(毎分など)で取り出して実行
}・onEdit/onOpen/カスタム関数に重処理を載せると即Timeoutに直結。
Advanced Sheets APIでバッチ更新(呼び出し回数の圧縮)
// 有効化後に利用
function batchWrite(sheetId) {
const body = {
valueInputOption: 'USER_ENTERED',
data: [
{ range: 'Data!A1', values: [['A','B'],['C','D']] },
{ range: 'Data!D1', values: [[1],[2]] }
]
};
Sheets.Spreadsheets.Values.batchUpdate(body, sheetId);
}・SpreadsheetApp多発より短時間で終了しやすい。
外部API側の制約・遅延に備える(設計の指針)
/*
・APIのページングを使い、1回のレスポンスを軽くする
・If-None-Match / If-Modified-Since など条件付リクエストがあるなら活用
・重い集計は外部で済ませ、GAS側は取得と整形のみ
・CacheService/PropertiesService/Driveファイルで結果を再利用
*/・「毎回フル取得・重加工」はTimeoutの定番パターン。差分取得とキャッシュで抑える。
よくあるNG→OK(クイック修正)
× 100件のAPIを直列 fetch → ○ fetchAll で並列 + timeout + 再試行
× 1セルずつ setValue → ○ setValues で一括 + チャンク分割
× onEdit で重い集計 → ○ キューに積み、時間トリガーでバッチ処理
× 最後まで走り切ろうとする → ○ 締切監視で自発停止→次回続き
× 並行で同じシートを書き換え → ○ LockService で排他再現→解消の通し例(外部API+シート書き込み)
// NG:直列fetch + 逐次書き込みでタイムアウト
function syncSlow(urls) {
const sh = SpreadsheetApp.openById('ID').getSheetByName('Data');
urls.forEach((u, i) => {
const res = UrlFetchApp.fetch(u); // 待ちが積み上がる
sh.getRange(i+1, 1).setValue(res.getContentText()); // 逐次書き込み
});
}
// OK:並列fetch + 一括書き込み + 締切管理
function syncFast(urls) {
const DEADLINE = Date.now() + 55_000;
const ss = SpreadsheetApp.openById('ID');
const sh = ss.getSheetByName('Data');
const bodies = withRetry(() => fetchParallel(urls)); // 並列 + 再試行
const values = bodies.map(b => [b.code, (b.body || '').slice(0, 500)]);
if (Date.now() > DEADLINE) {
// 次回に回す設計(省略可)
scheduleNext(1);
return;
}
sh.getRange(1, 1, values.length, values[0].length).setValues(values); // 一括
}チェックリスト(上から順に)
□ 遅延区間を計測して特定したか(UrlFetch/Sheets/Drive など)
□ 外部リクエストは fetchAll + timeout + 再試行にしたか
□ スプレッドシート操作は getValues/setValues の一括・分割にしたか
□ 締切管理(自発停止→トリガー継続)を入れたか
□ onEdit/onOpen など短い実行枠に重処理を置いていないか
□ LockService で同時実行の競合を避けているか
□ キャッシュ/差分取得で無駄な処理を省いているか
□ Advanced Sheets API のバッチ更新に置き換えられる箇所はあるか-
前の記事
GAS『Cannot Call Method on Null』の原因と対処法 2025.10.14
-
次の記事
GAS『Authorization is Required』の原因と対処法 2025.10.17
コメントを書く