【GAS】Google Apps Scriptの実行時間上限(6分制限)を回避する非同期分散・トリガー分割テクニック

Googleスプレッドシートのデータを大量に処理したり、外部Web APIへ連続でリクエストを送ったりしている際、突然処理が停止して Exceeded maximum execution time というエラーが出た経験はないでしょうか。

Google Apps Script(GAS)には、1回のスクリプト実行時間に対する厳格な上限制限(標準アカウントで6分)が設定されています。大量のデータ処理では、単純な for ループで一括処理を行おうとすると必ずこの壁に突き当たります。

この記事では、PropertiesService(プロパティサービス)を活用した進捗状態の記録と、ScriptApp による時間主導型トリガーの動的生成を組み合わせ、6分制限を回避して大規模な処理を完全自動で完遂させるテクニックを解説します。

発生したエラーと背景

GASで大量のデータ更新や外部API連携を実行すると、開始から約6分が経過した時点で以下のエラーが発生し、処理が強制終了します。

Exceeded maximum execution time
(最大の実行時間を超えました)

このエラーが発生すると、どこまで処理が成功したかが不明になり、途中のデータが破損したり、二重処理が発生したりする原因になります。

この記事で行うこと

  • GASの実行時間制限(6分上限)の仕組みの理解
  • PropertiesService を用いた進捗状況(処理済み行数)の保存と復元
  • 実行時間をスクリプト内で監視し、5分経過時に安全にループを一時中断する実装
  • ScriptApp で時間主導型トリガーを自動生成し、1分後に処理を自動再開させるロジックの構築
  • 処理完了時のトリガーおよびプロパティの自動クリーンアップの実装

前提条件と検証環境

この記事の内容は以下の環境を想定しています。

項目条件・内容
プラットフォームGoogle Apps Script(GAS) / Googleスプレッドシート
アカウント種別個人用Googleアカウント(無料版) / Google Workspaceアカウント
標準実行上限時間無料アカウント:6分/回、Google Workspace:6分/回

なぜ発生するのか?GASの実行時間上限(6分制限)

Googleが提供するクラウド共有環境であるGASは、特定のユーザーがサーバーリソースを独占するのを防ぐため、1回の実行時間の上限が定められています。

アカウントタイプ1回あたりの最大実行時間
無料のGoogleアカウント(@gmail.com)6分(360秒)
Google Workspace アカウント6分(360秒)

「30分」の実行制限は過去に一部の環境で報告されたことがありますが、2026年8月現在の公式ドキュメントにおいて、Workspace含む全アカウントで共通の上限は6分とされています。スクリプトを設計する際は、アカウント種別を問わず6分を上限として設計することが推奨されています。

スクリプト内で Utilities.sleep() を使って待機している時間もこの実行時間に含まれるため、外部通信や重い処理を行う場合はすぐに6分を超過してしまいます。

6分制限を回避する「非同期分散・トリガー分割」の仕組み

この制限を根本的に回避するためには、単一の長い処理を以下のように分割して繰り返し実行するアプローチをとります。

  1. 進捗保存: 処理を開始する際、PropertiesService から前回中断したインデックス(行番号)を取得する。
  2. 時間監視: ループ処理中に経過時間を計算し、安全圏である「5分(300秒)」に達したら処理を一時中断する。
  3. 次回インデックス更新: 中断した時点のインデックスを PropertiesService に保存する。
  4. 自動再開予約ScriptApp.newTrigger() を使って「1分後に自分自身(メイン関数)を再実行するトリガー」を動的作成する。
  5. クリーンアップ: データ全体の処理が終わったら、作成したトリガーと保存した進捗プロパティをきれいに削除する。

コピペで使える!分割バッチ処理の実践コード

以下は、Googleスプレッドシートの大量行をバッチ分割して安全に全件処理する標準的な実装パターンです。

/**
 * 分割バッチ処理のメイン関数
 */
function processBatch() {
  const startTime = new Date().getTime();
  const maxExecutionTime = 5 * 60 * 1000; // 安全のため5分(300,000ミリ秒)で中断
  const scriptProperties = PropertiesService.getScriptProperties();
  
  // 保存されている次回開始インデックスを取得(初期値は2行目とし、ヘッダーをスキップ)
  let startRow = parseInt(scriptProperties.getProperty('LAST_PROCESSED_ROW') || '2', 10);
  
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();
  
  console.log(`処理を開始します。開始行: ${startRow} / 全体行: ${lastRow}`);
  
  for (let currentRow = startRow; currentRow <= lastRow; currentRow++) {
    // ---- 実際のデータ処理(例:API呼び出しやセル書き込み) ----
    const cellValue = sheet.getRange(currentRow, 1).getValue();
    
    // 重い処理のダミー(実際にはここに入力・更新処理を書きます)
    // Utilities.sleep(1000); 
    sheet.getRange(currentRow, 2).setValue(`処理済み: ${new Date().toLocaleTimeString()}`);
    
    // --------------------------------------------------
    
    // 実行時間のチェック(5分を超えたら安全に一時中断)
    const currentTime = new Date().getTime();
    if (currentTime - startTime > maxExecutionTime) {
      console.log(`安全制限の5分に達したため、${currentRow}行目で一時中断します。`);
      
      // 次回開始する行数を保存
      scriptProperties.setProperty('LAST_PROCESSED_ROW', (currentRow + 1).toString());
      
      // 1分後に自動再開するトリガーを登録
      createNextTrigger();
      return;
    }
  }
  
  // すべての処理が完了した場合
  console.log('すべての処理が正常に完了しました。');
  cleanupBatchProcess();
}

/**
 * 1分後に再実行する時間主導型トリガーを動的に作成
 */
function createNextTrigger() {
  // 既存の同名トリガーが残っている場合は削除(二重起動防止)
  deleteExistingTriggers();
  
  // 1分後に processBatch を実行するトリガーを作成
  ScriptApp.newTrigger('processBatch')
    .timeBased()
    .after(1 * 60 * 1000)
    .create();
    
  console.log('1分後の自動再開トリガーを設定しました。');
}

/**
 * processBatch 用の動的トリガーを削除する
 */
function deleteExistingTriggers() {
  const triggers = ScriptApp.getProjectTriggers();
  for (let i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() === 'processBatch') {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

/**
 * 全処理完了時のクリーンアップ
 */
function cleanupBatchProcess() {
  const scriptProperties = PropertiesService.getScriptProperties();
  scriptProperties.deleteProperty('LAST_PROCESSED_ROW');
  deleteExistingTriggers();
  console.log('プロパティとトリガーのクリーンアップが完了しました。');
}

コードのポイント解説

1. タイムアウト値(5分)の設定

GASの上限は6分(360秒)ですが、予期せぬネットワーク遅延やクリーンアップ処理の時間を考慮し、スクリプト内での中断判定閾値は 5分(300,000ミリ秒) に設定するのがベストプラクティスです。

2. PropertiesService による状態管理

PropertiesService.getScriptProperties() を利用して、最後に処理した行番号を永続化しています。スクリプトがタイムアウトで終了しても、プロパティ値はクラウド上に保存されるため、次回起動時にその続きからシームレスに処理を再開できます。

3. 二重起動を防ぐトリガー管理

トリガーを生成する前に deleteExistingTriggers() で古い同名トリガーを削除しています。これを怠ると、エラーなどで過去に作成されたトリガーが重複して残り、意図しない頻度でスクリプトが複数同時起動してしまう事故につながります。

注意点と制限事項

このテクニックを使用する際は、以下のシステム制限に注意してください。

  • 1日の総トリガー実行時間: 実行時間を分割しても、1日あたりのトリガー合計実行時間上限(無料アカウントで90分/日、Workspaceで6時間/日)を超過することはできません。
  • PropertiesService の容量制限: 1つのプロパティ値に保存できる文字列長は9KB(約9,000文字)までです。行番号などの単一数値なら問題ありませんが、巨大なJSONオブジェクトなどを保存する場合はご注意ください。
  • スプレッドシートの書き込み回数: ループ内で setValue() を毎回実行すると処理速度が著しく低下します。可能であれば配列にまとめて setValues() で一括更新するか、定期的に書き込む工夫を併用してください。

まとめ

GASの「Exceeded maximum execution time(6分制限)」エラーは、コードの書き方を少し工夫するだけで確実に回避できます。

  • 実行開始時間を記録し、5分経過時点で自発的に処理を一時中断する
  • PropertiesService に進捗状態(次回の開始行など)を保存する
  • ScriptApp.newTrigger() で1分後の再開トリガーを動的設定する
  • 完了時にはトリガーと保存プロパティを忘れずに削除・クリーンアップする

このパターンを一度覚えておけば、数万件規模のスプレッドシート処理や大容量のAPIデータ同期も安定して全自動実行できるようになります。ぜひ活用してみてください。

参考情報