Google Apps Script(GAS)でスプレッドシートやGmail、Driveをまとめて処理していると、突然
Exceeded maximum execution time
というエラーが出てスクリプトが途中で止まることがある。このエラーは「コードが壊れた」というより、GAS側が用意している「1回の実行時間の上限」を超えてしまったことを意味している。ここでは、GASに特有の実行時間制限の考え方と、現実的な回避パターンをまとめる。
Exceeded maximum execution time の意味と制限時間
GASでは、1回の実行で使える時間に上限が決められている。代表的な目安は次の通り。
- 単発実行(エディタからの実行・単純トリガー・時間主導トリガーなど):おおむね 6分程度
- Webアプリ・カスタム関数なども、基本的には数分〜十数分の範囲で制限される
つまり、コード自体に文法的な問題が無くても、6分を超えて動き続けた時点で容赦なく止められる。
このため、実行時間エラーを”直す”というより、6分以内に終わるように処理を組み立て直すことが本質的な対処になる。
よくある「時間切れ」パターン
実行時間超過を起こしやすいのは、次のような処理である。
- スプレッドシートの数千〜数万行を、
forループで1行ずつgetValue()/setValue()している - Gmail の大量メールを1通ずつ読み、条件でラベル付けや転送をしている
- Driveの大量ファイルに対して1件ずつコピーや移動を行っている
- 外部APIに何百回もリクエストを投げている
これらに共通するのは、**「回数の多いI/O(読み書き)」と「長いループ」**である。GASの実行時間を短縮するには、ここをまとめる・分割する設計に変える必要がある。
基本方針:減らす・速くする・分割する
時間超過対策は、次の3つを押さえると考えやすい。
- 処理そのものを減らす
- 本当に必要な行・列だけを対象にする
- 条件チェックの前に早めに
continueでスキップする
- 1回あたりの処理を速くする(まとめて扱う)
- スプレッドシートは
getValues()/setValues()でまとめて読み書きする - ループの中で
SpreadsheetAppやGmailAppを何度も呼ばない
- スプレッドシートは
- 処理を分割して複数回に分ける
- 1回の実行で全部終わらせず、100行ずつなど「バッチ処理」にする
- 進捗を
PropertiesServiceに保存しておき、次の実行で続きから処理する
実用的な回避パターン①:バッチ処理+途中位置の保存
スプレッドシートの処理を例に、100行ずつ処理するバッチ実行のパターンを見てみる。
function processBatch() {
const sheet = SpreadsheetApp.getActiveSheet();
const props = PropertiesService.getScriptProperties();
// 前回どこまで処理したかを取得(デフォルトは2行目から)
const startRow = Number(props.getProperty('CURRENT_ROW') || 2);
const batchSize = 100; // 1回で処理する行数
const lastRow = sheet.getLastRow();
if (startRow > lastRow) {
// 全行処理済み
props.deleteProperty('CURRENT_ROW');
Logger.log('All rows processed.');
return;
}
const endRow = Math.min(startRow + batchSize - 1, lastRow);
const range = sheet.getRange(startRow, 1, endRow - startRow + 1, 5); // 例: 5列分
const values = range.getValues();
// ここで values に対する処理をまとめて行う
for (let i = 0; i < values.length; i++) {
const row = values[i];
// 条件に応じた加工や判定など
// row[0], row[1] ... を書き換えていく
}
// 加工後の values を一括で書き戻す
range.setValues(values);
// 次回の開始行を保存
props.setProperty('CURRENT_ROW', String(endRow + 1));
Logger.log(`Processed rows ${startRow} to ${endRow}.`);
}このようにしておけば、1回実行ごとに100行までしか処理しないため、1回の実行時間を短く保ちやすい。時間主導トリガー(例: 5分ごと)を設定すれば、自動的に少しずつ処理を進めることができる。
実用的な回避パターン②:トリガーを使って「分割実行」
バッチ処理と相性が良いのが時間主導トリガーである。例えば、
processBatch()を5分おきに実行するトリガーを作成- 毎回100行だけ処理し、進捗は
PropertiesServiceで管理
とすることで、ユーザーが待ち続けることなく、裏側で処理が進む仕組みを作れる。実行時間制限を超えない範囲で、少しずつ処理していくイメージである。
実用的な回避パターン③:I/Oをループから追い出す
ループ内のI/O(特にスプレッドシート操作)は実行時間の大きなボトルネックになる。次のようなパターンは避けたい。
// 悪い例
for (let i = 2; i <= lastRow; i++) {
const value = sheet.getRange(i, 1).getValue();
// ...処理...
sheet.getRange(i, 2).setValue(result);
}これを、次のようにまとめて扱う形に変えるだけで、体感できるレベルで速度が変わる。
// 良い例
const range = sheet.getRange(2, 1, lastRow - 1, 2); // A,B列
const values = range.getValues();
for (let i = 0; i < values.length; i++) {
const value = values[i][0];
// ...処理...
values[i][1] = /* result */;
}
range.setValues(values);GmailやDriveでも、同様に「まとめて取得→ループ内でメモリ上のデータを処理→必要ならまとめて書き戻す」という形を取るほうが、実行時間制限に引っかかりにくい。
Exceeded maximum execution time が出たときのチェックリスト
エラーを見たときは、次の順番で確認すると原因を特定しやすい。
- どのAPI呼び出しが一番重いかを把握する
- スプレッドシートか、Gmailか、Driveか、外部APIか
- ループ内で同じサービスを何度も呼んでいないか確認する
getRange/getValue/setValueがループの中に連発していないか
- 処理対象件数を減らせないか検討する
- 対象シート・ラベル・フォルダを絞る
- すでに処理済みのものをスキップできないか考える
- バッチ処理に分割できないか考える
- 100件単位・200件単位などの小さい塊で処理する
- 進捗の保存とトリガーによる分割実行を検討する
PropertiesServiceやシート上の管理用セルで「次回の開始位置」を保存する
このチェックを順に当てはめれば、多くのケースで「どこを直せばよいか」が見えてくるはずである。
まとめ
GASにおける“Exceeded maximum execution time”は、バグというよりも「設計をもう一段スケーラブルにしよう」というサインに近い。処理の対象件数が増えたり、運用期間が延びたりすると、いずれ6分という壁にぶつかる。だからこそ、早い段階で
- まとめて読み書きする
- バッチ処理に分割する
- トリガーを使って裏側で処理を進める
といった発想を取り入れておくと、後々のメンテナンスコストを大きく下げられる。