GAS『Exceeded Maximum Execution Time』の原因と対処法
- 作成日 2025.10.09
- その他
Google Apps Scriptの実行が許可時間を超えると発生する。主因は「大量データの逐次処理」「SpreadsheetやDriveへの過剰なAPI呼び出し」「外部APIの待ち時間」「同時実行による競合」など。処理の分割・バッチ化・再実行設計・計測とチューニングで回避できる。
目次
- 1. エラーの意味と発生条件
- 2. まずやること:時間の可視化(どこで詰まっているか)
- 3. 最短復旧:処理を分割して複数回に分ける(チャンク & 継続)
- 4. スプレッドシート操作の高速化(getValues/setValuesの一括)
- 5. 外部APIの待ち時間短縮(fetchAll・バックオフ・タイムアウト)
- 6. 時間ガード:実行時間の上限前に安全停止→続きは自動起動
- 7. トリガー設計:短いエントリでは重い処理を走らせない
- 8. 同時実行の競合を避ける(LockService)
- 9. アルゴリズムを見直す(探索はMap/Set、まとめてJOINする)
- 10. Advanced Sheets APIで一括更新(batchUpdate/values.batchUpdate)
- 11. キャッシュ/保存で再計算を避ける(CacheService/Properties/Drive)
- 12. よくあるNG→OK(クイック修正)
- 13. 再現→解消の通し例(10万行の加工を安全に完走)
- 14. チェックリスト(上から順に潰す)
エラーの意味と発生条件
・1回の実行時間が制限を超えた(実行形態やトリガー種別で上限は異なる)
・onEdit/onOpen/カスタム関数など短時間上限の実行で重い処理を走らせた
・スプレッドシートを1セルずつ読み書きして処理時間を浪費
・UrlFetchAppで多数の外部APIを直列で呼び出し待ち時間が積み上がった
・重いループ/再計算/正規表現でCPU時間を使い切ったまずやること:時間の可視化(どこで詰まっているか)
// 区間ごとの所要時間をログに出す(console.time系はV8で利用可)
function timed(fn, label) {
const t0 = Date.now(); const r = fn(); const ms = Date.now() - t0;
console.log(`⏱ ${label}: ${ms}ms`); return r;
}
function example() {
timed(() => SpreadsheetApp.openById('...'), 'openById');
// 以降、重い区間にtimedを挟む
}・どの区間が支配的かを特定して、そこから対処する。
最短復旧:処理を分割して複数回に分ける(チャンク & 継続)
// 1回で全部やらず、n件ずつ処理。再開位置(cursor)はPropertiesに保存。
function processInChunks() {
const props = PropertiesService.getScriptProperties();
const cursor = Number(props.getProperty('cursor') || 2); // 2行目から
const CHUNK_ROWS = 1000;
const DEADLINE = Date.now() + 60_000; // 余裕をもって1分で切り上げ
const sh = SpreadsheetApp.openById('YOUR_SHEET_ID').getSheetByName('Data');
const last = sh.getLastRow();
const end = Math.min(cursor + CHUNK_ROWS - 1, last);
if (end < cursor) return;
const values = sh.getRange(cursor, 1, end - cursor + 1, sh.getLastColumn()).getValues();
// ... valuesを処理 ...
// 継続判定:締め切りが近い/まだ残りがある → 次回へ
if (end < last || Date.now() > DEADLINE) {
props.setProperty('cursor', String(end + 1));
scheduleNext(1); // 1分後に続き
} else {
props.deleteProperty('cursor'); // 完了
}
}
function scheduleNext(minutes) {
ScriptApp.newTrigger('processInChunks')
.timeBased().after(minutes * 60_000).create();
}・「締め切り前に自発的に中断→続きは次回」の型に切り替える。
スプレッドシート操作の高速化(getValues/setValuesの一括)
// NG:1セルずつアクセス(遅い/上限にかかりやすい)
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
sh.getRange(start + r, 1 + c).setValue(data[r][c]);
}
}
// OK:矩形で一括読み書き
const range = sh.getRange(start, 1, data.length, data[0].length);
range.setValues(data);
// 読みも一括してメモリ上で処理してから一括で書く
const values = sh.getDataRange().getValues();
// ... map/filter/reduce で加工 ...
sh.getRange(1, 1, values.length, values[0].length).setValues(values);
・「往復回数を減らす」ことが最も効く。可能なら列/行で塊にする。
外部APIの待ち時間短縮(fetchAll・バックオフ・タイムアウト)
// 複数リクエストはfetchAllで同時に投げる(外部の応答待ち時間を短縮)
function fetchParallel(urls) {
const reqs = urls.map(u => ({url: u, muteHttpExceptions: true, followRedirects: true}));
const res = UrlFetchApp.fetchAll(reqs);
return res.map(r => ({code: r.getResponseCode(), body: r.getContentText()}));
}
// リトライ(指数バックオフ)
function withRetry(fn, tries = 5) {
let wait = 400;
for (let i = 0; i < tries; i++) {
try { return fn(); } catch (e) {
if (i === tries - 1 || !/timeout|rate|quota|internal/i.test(e.message)) throw e;
Utilities.sleep(wait); wait = Math.min(wait * 2, 8000);
}
}
}・直列fetchの積み上がりを避け、失敗は短い待ち→再試行で復帰させる。
時間ガード:実行時間の上限前に安全停止→続きは自動起動
// 実行開始時刻を記録し、ループ途中で締切を監視
function guardedLoop() {
const started = Date.now();
const LIMIT = 5 * 60_000; // 想定上限より手前に設定
for (let i = 0; i < 999999; i++) {
// ... 処理 ...
if (Date.now() - started > LIMIT) {
PropertiesService.getScriptProperties().setProperty('resumeIndex', String(i));
scheduleNext(1);
return; // ここで終了(次回続き)
}
}
}・「自分で止まる→次へ」を入れると、Exceededを回避しつつ完走できる。
トリガー設計:短いエントリでは重い処理を走らせない
/*
・onEdit/onOpen/カスタム関数は短時間上限。重い処理は:
1) 軽い検証だけ行ってキュー(Properties/シート)に積む
2) 時間主導トリガー(毎分/5分)やボタンクリックでキューをバッチ処理
*/
function enqueueJob(payload) {
const cache = CacheService.getScriptCache();
cache.put('job', JSON.stringify(payload), 60); // 60秒キャッシュなど
}・「即時処理」から「バッチ処理」に発想を切り替える。
同時実行の競合を避ける(LockService)
function job() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(30_000)) return; // 先行ジョブに譲る
try {
// 排他が必要なシート/ファイル更新
} finally { lock.releaseLock(); }
}・重複実行で処理時間が伸びる/エラーが出るのを防ぐ。
アルゴリズムを見直す(探索はMap/Set、まとめてJOINする)
// NG:都度find(O(n^2)になりがち)
const out = [];
for (const row of left) {
const hit = right.find(r => r[0] === row[0]);
out.push([row[0], hit ? hit[1] : '']);
}
// OK:事前にMapで索引を作る(O(n))
const idx = new Map(right.map(r => [r[0], r[1]]));
const out2 = left.map(row => [row[0], idx.get(row[0]) ?? '']);・計算量の改善は「時間制限」対策として効果が大きい。
Advanced Sheets APIで一括更新(batchUpdate/values.batchUpdate)
// サービス(リソース)を有効化してから利用
function batchClearAndWrite(sheetId) {
const requests = [{ updateCells: { range: { sheetId: 0 }, fields: 'userEnteredValue' } }];
Sheets.Spreadsheets.batchUpdate({requests}, sheetId);
const body = {
valueInputOption: 'USER_ENTERED',
data: [{ range: 'Data!A1', values: [['A','B'],['C','D']]}]
};
Sheets.Spreadsheets.Values.batchUpdate(body, sheetId);
}・SpreadsheetAppの多回呼び出しをまとめて短時間で終わらせる。
キャッシュ/保存で再計算を避ける(CacheService/Properties/Drive)
// 高価な取得結果をキャッシュ
function getUsers() {
const cache = CacheService.getScriptCache();
const key = 'users';
let json = cache.get(key);
if (!json) {
const users = AdminDirectory.Users.list({domain: 'example.com'}).users;
json = JSON.stringify(users);
cache.put(key, json, 300); // 5分キャッシュ
}
return JSON.parse(json);
}・同じ計算/取得を繰り返さない。
よくあるNG→OK(クイック修正)
× 1セルずつ setValue → ○ setValues で一括
× DataRange を何度も取得 → ○ 1回だけ取得して配列で処理
× 直列UrlFetch を大量実行 → ○ fetchAll で同時実行 + バックオフ
× onEdit で重処理 → ○ キューに積んで時間トリガーで処理
× 最後まで走り切ろうとする → ○ 締切前に停止し、次回へ継続再現→解消の通し例(10万行の加工を安全に完走)
// 1) 10万行をチャンク処理+継続
function runLargeJob() {
const props = PropertiesService.getScriptProperties();
const row = Number(props.getProperty('row') || 2);
const CHUNK = 2000, deadline = Date.now() + 60_000;
const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
const sh = ss.getSheetByName('Data');
const last = sh.getLastRow();
const end = Math.min(row + CHUNK - 1, last);
if (end >= row) {
const values = sh.getRange(row, 1, end - row + 1, sh.getLastColumn()).getValues();
// 加工例:2列目を大文字化
for (let i = 0; i < values.length; i++) values[i][1] = String(values[i][1] || '').toUpperCase();
sh.getRange(row, 1, values.length, values[0].length).setValues(values);
}
if (end < last && Date.now() < deadline) {
props.setProperty('row', String(end + 1));
runLargeJob(); // 余裕があれば再帰で次チャンク
} else if (end < last) {
props.setProperty('row', String(end + 1));
scheduleNext(1); // 時間切れ → 次回
} else {
props.deleteProperty('row'); // 完了
}
}チェックリスト(上から順に潰す)
□ 重い区間を計測し、ボトルネックを特定したか
□ Spreadsheet操作は一括(getValues/setValues)に置き換えたか
□ 外部APIはfetchAll/バックオフで待ち時間を圧縮したか
□ 処理をチャンク化し、締切前に自発停止→次回継続できるか
□ 短時間上限のトリガーに重処理を載せていないか
□ LockServiceで競合を防いでいるか
□ アルゴリズム(探索/結合/再計算)を見直したか
□ Advanced Sheets APIの一括更新を活用したか-
前の記事
GAS『ReferenceError: Function is Not Defined』の原因と対処法 2025.10.08
-
次の記事
GAS『Script Function Not Found』の原因と対処法 2025.10.10
コメントを書く