Google Apps Script(GAS)の6分制限を回避する方法【実践コード付き】

この記事でわかること

  • GASの実行時間制限とは何か
  • 6分制限を回避する3つの方法
  • 自動継続トリガーの実装方法
  • 進捗管理と手動停止・再開の実装
  • 本番で使えるコードテンプレート

この記事の対象読者

  • GASで大量データを処理したい方
  • 「起動時間の最大値を超えました」エラーに困っている方
  • 数千件〜数万件のデータを自動処理したい方
  • GASでバッチ処理を実装したい方

GASの実行時間制限とは

制限の内容

Google Apps Script(GAS)には、1回の実行につき最大6分(360秒) という時間制限があります。

アカウント種別実行時間の上限
通常のGoogleアカウント6分
Google Workspace6分(一部30分)

この制限を超えると、以下のエラーが発生して処理が強制終了します。

起動時間の最大値を超えました
(Exceeded maximum execution time)

なぜ制限があるのか

GASはGoogleのサーバー上で実行されるため、リソースを独占しないよう制限が設けられています。

問題になるケース

ケース
大量データの処理数千行のスプレッドシートを1行ずつ処理
外部API連携1件ずつAPIを叩いて数百件のデータを取得
ファイル操作大量のファイルをGoogle Driveにアップロード
メール送信数百件のメールを1通ずつ送信

回避方法の比較

6分制限を回避する方法は主に3つあります。

方法概要難易度おすすめ度
①トリガーで自動継続5分で中断→1分後に再開★★☆
②処理を分割して手動実行1000件ずつ分けて実行★☆☆
③並列処理複数の関数を同時実行★★★

本記事では、最も汎用的な「①トリガーで自動継続」の方法を詳しく解説します。

方法①:トリガーで自動継続(推奨)

基本的な考え方

処理開始
   ↓
[1件ずつ処理]
   ↓ 5分経過
現在位置を保存
   ↓
1分後に再開するトリガーを作成
   ↓
処理終了
   ↓ (1分後)
トリガーが発火
   ↓
保存した位置から再開
   ↓
(全件完了まで繰り返し)

ポイント

  • 5分で中断:6分制限に余裕を持たせる
  • 位置を保存:どこまで処理したかを記録
  • トリガーで再開:自動的に続きから処理

実装:基本形

最小構成のコード

まずは最小構成のコードを見てみましょう。

javascript

/**
 * メイン処理(基本形)
 */
function processData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('データ');
  const data = sheet.getDataRange().getValues();
  
  // 開始位置を取得(保存されていなければ1から)
  const properties = PropertiesService.getScriptProperties();
  const startRow = parseInt(properties.getProperty('currentRow') || '1');
  
  // タイムアウト設定(5分 = 300,000ミリ秒)
  const TIMEOUT_MS = 5 * 60 * 1000;
  const startTime = new Date();
  
  // データを処理
  for (let i = startRow; i < data.length; i++) {
    
    // タイムアウトチェック
    if (new Date() - startTime > TIMEOUT_MS) {
      // 現在位置を保存
      properties.setProperty('currentRow', String(i));
      
      // 1分後に再開するトリガーを作成
      ScriptApp.newTrigger('processData')
        .timeBased()
        .after(1 * 60 * 1000)
        .create();
      
      console.log(`タイムアウト: ${i}行目で中断。1分後に再開します。`);
      return;
    }
    
    // ここに実際の処理を書く
    processRow(data[i], i);
  }
  
  // 完了:保存した位置をクリア
  properties.deleteProperty('currentRow');
  console.log('全件処理完了');
}

/**
 * 1行の処理(例)
 */
function processRow(row, index) {
  // 実際の処理をここに書く
  console.log(`処理中: ${index}行目`);
  Utilities.sleep(100); // 処理の代わりに少し待機
}

解説

要素説明
PropertiesService現在位置を保存するためのストレージ
TIMEOUT_MSタイムアウト時間(5分に設定)
ScriptApp.newTrigger()時間指定でトリガーを作成
.after(1 * 60 * 1000)1分後に実行

実装:本番向け(完全版)

実際の業務で使えるよう、以下の機能を追加した完全版を紹介します。

  • ✅ 進捗状況の表示
  • ✅ 手動での停止・再開
  • ✅ エラーハンドリング
  • ✅ ログ出力
  • ✅ 重複トリガーの防止

完全版コード

javascript

/**
 * ========================================
 * 設定
 * ========================================
 */
const CONFIG = {
  TIMEOUT_MS: 5 * 60 * 1000,  // 5分でタイムアウト
  TRIGGER_WAIT_MS: 1 * 60 * 1000,  // 1分後に再開
  SHEETS: {
    DATA: 'データ',
    SETTINGS: '設定',
    LOG: 'ログ'
  }
};

/**
 * ========================================
 * メニュー
 * ========================================
 */
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('🔄 バッチ処理')
    .addItem('▶️ 処理を開始', 'startProcessing')
    .addItem('⏹️ 処理を停止', 'stopProcessing')
    .addItem('🔄 停止した処理を再開', 'resumeProcessing')
    .addSeparator()
    .addItem('🔃 最初からやり直す', 'resetProcessing')
    .addToUi();
}

/**
 * ========================================
 * 設定値の管理
 * ========================================
 */
function getSettingValue(ss, key) {
  const sheet = ss.getSheetByName(CONFIG.SHEETS.SETTINGS);
  if (!sheet) return null;
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    if (data[i][0] === key) return data[i][1];
  }
  return null;
}

function setSettingValue(ss, key, value) {
  const sheet = ss.getSheetByName(CONFIG.SHEETS.SETTINGS);
  if (!sheet) return;
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    if (data[i][0] === key) {
      sheet.getRange(i + 1, 2).setValue(value);
      return;
    }
  }
}

/**
 * ========================================
 * ログ出力
 * ========================================
 */
function addLog(ss, message, type = 'INFO') {
  const sheet = ss.getSheetByName(CONFIG.SHEETS.LOG);
  if (!sheet) return;
  sheet.appendRow([new Date(), type, message]);
}

/**
 * ========================================
 * トリガー管理
 * ========================================
 */
function deleteContinueTriggers() {
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'continueProcessing') {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

function createContinueTrigger() {
  // 既存のトリガーを削除(重複防止)
  deleteContinueTriggers();
  
  // 新しいトリガーを作成
  ScriptApp.newTrigger('continueProcessing')
    .timeBased()
    .after(CONFIG.TRIGGER_WAIT_MS)
    .create();
}

/**
 * ========================================
 * メイン処理
 * ========================================
 */
function startProcessing() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // 初期化
  setSettingValue(ss, '処理中の行番号', '1');
  setSettingValue(ss, '停止フラグ', 'FALSE');
  setSettingValue(ss, '処理状況', '⏳ 処理中...');
  
  addLog(ss, '処理を開始しました', 'START');
  
  // 処理実行
  doProcess(ss);
}

function continueProcessing() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // 停止フラグをチェック
  if (getSettingValue(ss, '停止フラグ') === 'TRUE') {
    addLog(ss, '停止フラグにより処理を中止しました', 'STOP');
    return;
  }
  
  // 処理実行
  doProcess(ss);
}

function doProcess(ss) {
  const sheet = ss.getSheetByName(CONFIG.SHEETS.DATA);
  const data = sheet.getDataRange().getValues();
  
  // 開始位置を取得
  const startRow = parseInt(getSettingValue(ss, '処理中の行番号') || '1');
  const startTime = new Date();
  
  let processedCount = 0;
  
  for (let i = startRow; i < data.length; i++) {
    
    // 停止フラグをチェック(定期的に)
    if (processedCount % 10 === 0) {
      if (getSettingValue(ss, '停止フラグ') === 'TRUE') {
        setSettingValue(ss, '処理中の行番号', String(i));
        setSettingValue(ss, '処理状況', '⏹️ 停止中');
        addLog(ss, `${i}行目で手動停止しました`, 'STOP');
        return;
      }
    }
    
    // タイムアウトチェック
    if (new Date() - startTime > CONFIG.TIMEOUT_MS) {
      setSettingValue(ss, '処理中の行番号', String(i));
      setSettingValue(ss, '処理状況', `⏳ 継続待ち... (${i}/${data.length - 1}件)`);
      addLog(ss, `タイムアウト: ${i}行目で中断、1分後に再開`, 'TIMEOUT');
      
      createContinueTrigger();
      return;
    }
    
    // 実際の処理
    try {
      processRow(data[i], i, ss);
      processedCount++;
      
      // 進捗更新(100件ごと)
      if (processedCount % 100 === 0) {
        setSettingValue(ss, '処理状況', `⏳ 処理中... (${i}/${data.length - 1}件)`);
      }
      
    } catch (e) {
      addLog(ss, `エラー (${i}行目): ${e.message}`, 'ERROR');
      // エラーでも続行(必要に応じて変更)
    }
  }
  
  // 完了処理
  setSettingValue(ss, '処理中の行番号', '');
  setSettingValue(ss, '処理状況', `✅ 完了 (${data.length - 1}件処理)`);
  addLog(ss, `処理が完了しました(${data.length - 1}件)`, 'COMPLETE');
  deleteContinueTriggers();
}

/**
 * 1行の処理(ここをカスタマイズ)
 */
function processRow(row, index, ss) {
  // ========================================
  // ここに実際の処理を書く
  // ========================================
  
  // 例:B列の値を取得してC列に書き込み
  const sheet = ss.getSheetByName(CONFIG.SHEETS.DATA);
  const value = row[1]; // B列
  sheet.getRange(index + 1, 3).setValue(`処理済み: ${value}`);
  
  // APIを叩く場合は適切な待機時間を入れる
  Utilities.sleep(100);
}

/**
 * ========================================
 * 停止・再開・リセット
 * ========================================
 */
function stopProcessing() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  setSettingValue(ss, '停止フラグ', 'TRUE');
  deleteContinueTriggers();
  
  SpreadsheetApp.getUi().alert('停止リクエストを送信しました。\n処理中の行が完了次第、停止します。');
}

function resumeProcessing() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  const currentRow = getSettingValue(ss, '処理中の行番号');
  if (!currentRow) {
    SpreadsheetApp.getUi().alert('再開する処理がありません。');
    return;
  }
  
  setSettingValue(ss, '停止フラグ', 'FALSE');
  setSettingValue(ss, '処理状況', '⏳ 再開中...');
  addLog(ss, `${currentRow}行目から処理を再開`, 'RESUME');
  
  doProcess(ss);
}

function resetProcessing() {
  const ui = SpreadsheetApp.getUi();
  const response = ui.alert('確認', '処理状況をリセットして最初からやり直しますか?', ui.ButtonSet.YES_NO);
  
  if (response !== ui.Button.YES) return;
  
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  setSettingValue(ss, '処理中の行番号', '');
  setSettingValue(ss, '停止フラグ', 'FALSE');
  setSettingValue(ss, '処理状況', '待機中');
  deleteContinueTriggers();
  
  addLog(ss, '処理状況をリセットしました', 'RESET');
  ui.alert('リセットしました。');
}

/**
 * ========================================
 * 初期セットアップ
 * ========================================
 */
function initialSetup() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // 設定シート作成
  let settingsSheet = ss.getSheetByName(CONFIG.SHEETS.SETTINGS);
  if (!settingsSheet) {
    settingsSheet = ss.insertSheet(CONFIG.SHEETS.SETTINGS);
  }
  settingsSheet.clear();
  settingsSheet.getRange(1, 1, 5, 2).setValues([
    ['項目', '値'],
    ['処理中の行番号', ''],
    ['停止フラグ', 'FALSE'],
    ['処理状況', '待機中'],
    ['最終更新', new Date()]
  ]);
  settingsSheet.getRange(1, 1, 1, 2).setBackground('#4285f4').setFontColor('white');
  
  // ログシート作成
  let logSheet = ss.getSheetByName(CONFIG.SHEETS.LOG);
  if (!logSheet) {
    logSheet = ss.insertSheet(CONFIG.SHEETS.LOG);
  }
  logSheet.clear();
  logSheet.getRange(1, 1, 1, 3).setValues([['日時', '種別', 'メッセージ']]);
  logSheet.getRange(1, 1, 1, 3).setBackground('#4285f4').setFontColor('white');
  
  SpreadsheetApp.getUi().alert('セットアップが完了しました。\n「データ」シートを作成して処理対象のデータを入れてください。');
}

設定シートの構成

上記のコードで使用する「設定」シートの構成:

項目説明
処理中の行番号(数値)中断時の再開位置
停止フラグTRUE/FALSE手動停止の制御
処理状況(テキスト)現在の状態を表示
最終更新(日時)最終更新日時

処理状況の表示例

表示意味
待機中処理を実行していない
⏳ 処理中... (500/4000件)処理を実行中
⏳ 継続待ち... (500/4000件)タイムアウトで中断、1分後に再開
⏹️ 停止中手動で停止した状態
✅ 完了 (4000件処理)全件処理完了

動作の流れ

1. ユーザーが「処理を開始」を実行
   ↓
2. 1件ずつ処理を実行
   ↓
3. 5分経過 → タイムアウト
   ↓
4. 現在位置を「設定」シートに保存
   ↓
5. 1分後に再開するトリガーを作成
   ↓
6. 処理終了
   ↓
(1分後)
   ↓
7. トリガーが発火 → continueProcessing() 実行
   ↓
8. 「設定」シートから位置を読み込み
   ↓
9. 続きから処理を再開
   ↓
(5分経過 → 3に戻る)
   ↓
10. 全件完了 → 完了ステータスを表示

カスタマイズのポイント

処理内容を変更する

processRow() 関数の中身を変更します。

javascript

function processRow(row, index, ss) {
  // 例1: 外部APIを叩く
  const response = UrlFetchApp.fetch('https://api.example.com/data');
  
  // 例2: メールを送信
  MailApp.sendEmail(row[1], '件名', '本文');
  
  // 例3: 別シートに書き込み
  const outputSheet = ss.getSheetByName('出力');
  outputSheet.appendRow([row[0], row[1], new Date()]);
}

タイムアウト時間を変更する

javascript

const CONFIG = {
  TIMEOUT_MS: 4 * 60 * 1000,  // 4分に変更
  TRIGGER_WAIT_MS: 2 * 60 * 1000,  // 2分後に再開
  // ...
};

停止チェックの頻度を変更する

javascript

// 10件ごと → 50件ごとに変更
if (processedCount % 50 === 0) {
  if (getSettingValue(ss, '停止フラグ') === 'TRUE') {
    // ...
  }
}

よくある質問

Q: ブラウザを閉じても処理は続きますか?

A: はい、続きます。

GASはGoogleのサーバー上で実行されるため、ブラウザを閉じても、PCをシャットダウンしても処理は継続します。

Q: 途中で止めたい場合は?

A: メニューから「処理を停止」を実行してください。

数秒以内に安全に停止します。停止後は「停止した処理を再開」で続きから再開できます。

Q: 同時に複数実行するとどうなりますか?

A: 競合が発生する可能性があります。

同時実行を防ぐには、ロック機構を追加します:

javascript

function startProcessing() {
  const lock = LockService.getScriptLock();
  const hasLock = lock.tryLock(1000);
  
  if (!hasLock) {
    SpreadsheetApp.getUi().alert('現在、処理を実行中です。');
    return;
  }
  
  try {
    doProcess(ss);
  } finally {
    lock.releaseLock();
  }
}

Q: エラーが発生したらどうなりますか?

A: ログに記録して続行します。

エラーで停止させたい場合は、processRow() 内の try-catch を削除してください。

まとめ

6分制限を回避するポイント

ポイント説明
タイムアウト検知5分経過したら処理を中断
位置の保存どこまで処理したか記録
トリガーで再開1分後に自動的に続きを実行
停止・再開機能手動で制御できるようにする
進捗表示ユーザーが状況を把握できる

この方法のメリット

  • ✅ 数千〜数万件のデータを自動処理可能
  • ✅ ブラウザを閉じても処理が継続
  • ✅ 途中停止・再開が可能
  • ✅ 進捗状況が確認できる

関連記事

もっと詳しく知りたい方へ

「大量データを処理するシステムを作りたい」 「GASで業務を自動化したい」

そのようなご要望があれば、お気軽にご相談ください。

AIDXの仕組みを社内で内製化したい方へ

スパルタAIDX研修で、現場で使えるAI実践スキルを短期集中で習得できます。
ベテランの技術継承や業務自動化を自社で実現したい方におすすめです。

AIDXの仕組みを構築を依頼したい方へ

貴社の課題に合わせた最適なソリューションをご提案いたします。
導入事例の詳細や、具体的な実装方法についてもお気軽にご相談ください。

CONTACT

資料請求・お問い合わせはこちらから

お気軽にご連絡ください