Google Apps Scriptにおける「Lock timeout: Could not obtain lock after 30 seconds」の解決方法

intermediate📗 Google Sheets2026-07-15| Google Apps Script (GAS), Google Sheets トリガー (onFormSubmit, onEdit, 時間主導型トリガー)。

Error Message

Lock timeout: Could not obtain lock after 30 seconds.
#google-apps-script#lockservice#optimization#google-sheets#concurrency

エラーのシナリオGoogle Sheetsを重要な登録フォームのバックエンドとして使用している場面を想像してください。自分一人でテストしている間は、すべて完璧に動作します。しかし、本番公開して50人以上のユーザーが同じ1分間に「送信」ボタンを押した瞬間、実行ログが赤く染まります。そして次のエラーに直面します:Lock timeout: Could not obtain lock after 30 seconds.

これは通常、スクリプトの複数のインスタンスが同じリソースを奪い合っているために発生します。ある実行インスタンスがロックを保持している間、他のすべてのインスタンスは列に並んで待機しなければなりません。その列が十分に早く進まない場合、Googleはプロセスを強制終了します。

発生の原因Google Apps Scriptは、2人のユーザーが同時に同じセルを上書きしてしまうような競合状態(レースコンディション)を防ぐために、LockServiceを使用します。lock.waitLock(30000)を呼び出すと、30秒のタイマーが設定されます。現在ロックを保持しているスクリプトが、その時間内に処理を終えてロックを解放しない場合、スクリプトは例外をスローして停止します。

根本的な原因は、通常「クリティカルセクション」(ロックを取得してから解放するまでの間のコード)が肥大化していることにあります。ロック中にスクリプトの実行に5秒かかる場合、7人目のユーザーが30秒のタイムアウトに達する前に処理できるのは、同時に6ユーザーまでです。

即効性のある修正:適切なエラーハンドリングスクリプトをクラッシュさせて、ユーザーに汎用的なエラーメッセージを表示したままにしてはいけません。ロックの試行をtry-catchブロックで囲みましょう。これにより、実行をサイレントに失敗させるのではなく、失敗をログに記録したり、ユーザーに通知したりできるようになります。

function processData() {
  var lock = LockService.getScriptLock();
  
  try {
    // 30秒間ロックの取得を試行
    lock.waitLock(30000);
  } catch (e) {
    console.warn('タイムアウト:ロックを取得できませんでした。サーバーの負荷が高い可能性があります。');
    // オプション:頻繁に発生する場合はメールでアラートを送信
    return;
  }

  // --- クリティカルセクション開始 ---
  // この領域をできるだけ軽量に保つ
  SpreadsheetApp.flush(); 
  // --- クリティカルセクション終了 ---

  lock.releaseLock();
}

根本的な修正:「ロックゾーン」を縮小する高パフォーマンスなスクリプトの秘訣は、待ち時間を長くすることではなく、ロック時間を短くすることです。スクリプトがロックを保持する時間を、数秒ではなくミリ秒単位にすることを目指します。

1. データの準備をロックの外に出す重い計算を行ったり、外部APIからデータを取得したりしている間にロックを保持してはいけません。必要な変数をすべて先に準備し、最後の書き込み操作のときだけロックを開くようにします。

// 回避すべき例:10秒かかるAPI呼び出し中にロックする
lock.waitLock(30000);
var response = UrlFetchApp.fetch(url); // 遅い!
sheet.appendRow([response]);
lock.releaseLock();

// 推奨例:先にフェッチし、0.2秒間だけロックする
var response = UrlFetchApp.fetch(url); 
var data = response.getContentText();

lock.waitLock(30000);
sheet.appendRow([data]); 
SpreadsheetApp.flush(); // 即座に書き込みを強制する
lock.releaseLock();

2. SpreadsheetApp.flush() を使用するGoogle Sheetsは、効率化のために変更をバッチ処理(まとめて処理)することがよくあります。しかし、ロック内では、それらの変更を即座にデータベースにコミットする必要があります。SpreadsheetApp.flush()を呼び出すことで、ロックが解放される前にデータが書き込まれたことを保証できます。これにより、次に並んでいるスクリプトが「古い」データを読み取るのを防ぎます。### 3. エクスポネンシャル バックオフの実装スクリプトが大量のトラフィックを一度に処理する場合、30秒の一度の待機では不十分な場合があります。短いランダムな遅延を入れたリトライループを実装できます。これにより、複数のスクリプトが全く同じミリ秒にロックを取得しようとする「サンダリングハード(群衆の殺到)」問題を防ぐことができます。

function robustLock() {
  var lock = LockService.getScriptLock();
  var success = false;
  var attempts = 0;

  while (!success && attempts < 3) {
    try {
      lock.waitLock(15000); // 15秒間試行
      success = true;
    } catch (e) {
      attempts++;
      // 再試行の前に500msから1500ms待機
      Utilities.sleep(Math.floor(Math.random() * 1000) + 500); 
    }
  }
  
  if (!success) throw "システムが混み合っています。しばらくしてからもう一度お試しください。";
  
  // ... 書き込み操作を実行 ...
  
  lock.releaseLock();
}

修正の確認Apps Scriptダッシュボードの実行数タブを確認して、変更が機能したかチェックします。以下の3つの指標に注目してください。

  • ステータス: 「タイムアウト」ではなく「完了」と表示されている必要があります。- 実行時間: 実行時間が一貫して30秒に達している場合、ロックが依然としてボトルネックになっています。- 並列性: console.log を使用して、エクスポネンシャル バックオフが実際に何回リトライを行っているかを追跡します。エラーが解消されない場合は、全員をブロックする getScriptLock() が本当に必要か、あるいはその特定のファイルのみをブロックする getDocumentLock() で十分かを検討してください。

Related Error Notes