GAS『Service Timeout』の原因と対処法

GAS『Service Timeout』の原因と対処法

Google Apps Script実行中に「Service Timeout」が出るのは、外部サービスやApps Scriptの各サービス呼び出しが規定時間内に応答しなかったため。主因はUrlFetchAppの遅延、Spreadsheet/Drive操作の過多、同時実行の競合、ネットワーク一時障害など。処理の分割・同時化・一括化・再試行・締切管理で回避する。

エラーの意味と発生条件

・UrlFetchApp / JDBC / Gmail / Calendar / Drive / Sheets などのサービス呼び出しが規定時間で返らない
・多数のAPI呼び出しを直列実行して待ち時間が累積
・スプレッドシートへの逐次アクセスで内部待ち(保護・競合・大量更新)
・同時実行や編集中の衝突で応答が遅延
・一時的なレート制限/内部エラーに伴う遅延

まず計測:どこで待っているかを特定

// 区間計測のミニユーティリティ
function timed(label, fn) {
  const t0 = Date.now();
  const out = fn();
  console.log(`⏱ ${label}: ${Date.now() - t0}ms`);
  return out;
}

// 使用例
function probe() {
  timed('openById', () => SpreadsheetApp.openById('YOUR_SHEET_ID'));
  timed('fetch', () => UrlFetchApp.fetch('https://example.com'));
}

・遅延区間が特定できれば、対処が明確になる。

UrlFetchAppを高速化:並列化・タイムアウト・再試行

// 1) 複数URLは fetchAll で並列
function fetchParallel(urls) {
  const reqs = urls.map(u => ({
    url: u,
    followRedirects: true,
    muteHttpExceptions: true,
    // タイムアウト指定(ms):長すぎる待ちを避ける
    timeout: 15000
  }));
  const res = UrlFetchApp.fetchAll(reqs);
  return res.map(r => ({ code: r.getResponseCode(), body: r.getContentText() }));
}

// 2) 再試行(指数バックオフ+ジッター)
function withRetry(fn, tries = 5) {
  let wait = 400;
  for (let i = 0; i < tries; i++) {
    try { return fn(); }
    catch (e) {
      const msg = e.message || '';
      const retryable = /timeout|timed out|Rate limit|quota|Internal|Backend/i.test(msg);
      if (!retryable || i === tries - 1) throw e;
      Utilities.sleep(wait + Math.floor(Math.random() * 200));
      wait = Math.min(wait * 2, 8000);
    }
  }
}

・直列fetchの積み上げはTimeoutの温床。fetchAllと短めtimeout+再試行で安定させる。

Spreadsheet操作の待ちを削減:一括読み書き+分割

// NG:逐次 setValue(内部待機が蓄積)
for (let r = 0; r < data.length; r++) {
  for (let c = 0; c < data[0].length; c++) {
    sh.getRange(start + r, 1 + c).setValue(data[r][c]);
  }
}

// OK:まとめて setValues
const range = sh.getRange(start, 1, data.length, data[0].length);
range.setValues(data);

// それでも大きいときはチャンク分割
function writeChunked(sh, start, values) {
  const MAX_CELLS = 50000; // 目安
  let i = 0;
  while (i < values.length) {
    const rows = Math.max(1, Math.min(values.length - i, Math.floor(MAX_CELLS / values[0].length)));
    const block = values.slice(i, i + rows);
    sh.getRange(start + i, 1, block.length, block[0].length).setValues(block);
    i += rows;
    Utilities.sleep(150); // 微スロットリング
  }
}

・往復回数とロック滞留を減らす。一気に書けない量は安全に分割。

締切管理:自発停止→次回続き(Service Timeoutの回避線)

function processWithDeadline() {
  const props = PropertiesService.getScriptProperties();
  const cursor = Number(props.getProperty('row') || 2);
  const DEADLINE = Date.now() + 55_000; // 実行上限より手前で終了
  const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
  const sh = ss.getSheetByName('Data');
  const last = sh.getLastRow();

  let row = cursor;
  while (row <= last) {
    // ... 行処理 ...
    row++;
    if (Date.now() > DEADLINE) {
      props.setProperty('row', String(row));
      scheduleNext(1); // 1分後に再開
      return;
    }
  }
  props.deleteProperty('row'); // 完了
}

function scheduleNext(minutes) {
  ScriptApp.newTrigger('processWithDeadline')
    .timeBased().after(minutes * 60_000).create();
}

・待ちが発生しそうな長処理は、上限前に自発終了→トリガーで継続。

同時実行の競合を避ける(LockService)

function job() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(30000)) return; // 先行実行に譲る
  try {
    // 競合しやすい範囲の更新
  } finally {
    lock.releaseLock();
  }
}

・ロック無しの並行更新は内部待ちを誘発し、Timeoutの引き金になる。

短時間トリガーでは重処理を行わない(キュー→バッチ)

// onEdit 内は軽い検証だけにしてキューへ積む
function onEdit(e) {
  if (!e || !e.range) return;
  const payload = { row: e.range.getRow(), time: Date.now() };
  const cache = CacheService.getScriptCache();
  cache.put('job', JSON.stringify(payload), 60); // 60秒保持
  // 実作業は時間トリガー(毎分など)で取り出して実行
}

・onEdit/onOpen/カスタム関数に重処理を載せると即Timeoutに直結。

Advanced Sheets APIでバッチ更新(呼び出し回数の圧縮)

// 有効化後に利用
function batchWrite(sheetId) {
  const body = {
    valueInputOption: 'USER_ENTERED',
    data: [
      { range: 'Data!A1', values: [['A','B'],['C','D']] },
      { range: 'Data!D1', values: [[1],[2]] }
    ]
  };
  Sheets.Spreadsheets.Values.batchUpdate(body, sheetId);
}

・SpreadsheetApp多発より短時間で終了しやすい。

外部API側の制約・遅延に備える(設計の指針)

/*
・APIのページングを使い、1回のレスポンスを軽くする
・If-None-Match / If-Modified-Since など条件付リクエストがあるなら活用
・重い集計は外部で済ませ、GAS側は取得と整形のみ
・CacheService/PropertiesService/Driveファイルで結果を再利用
*/

・「毎回フル取得・重加工」はTimeoutの定番パターン。差分取得とキャッシュで抑える。

よくあるNG→OK(クイック修正)

× 100件のAPIを直列 fetch        →  ○ fetchAll で並列 + timeout + 再試行
× 1セルずつ setValue             →  ○ setValues で一括 + チャンク分割
× onEdit で重い集計              →  ○ キューに積み、時間トリガーでバッチ処理
× 最後まで走り切ろうとする       →  ○ 締切監視で自発停止→次回続き
× 並行で同じシートを書き換え     →  ○ LockService で排他

再現→解消の通し例(外部API+シート書き込み)

// NG:直列fetch + 逐次書き込みでタイムアウト
function syncSlow(urls) {
  const sh = SpreadsheetApp.openById('ID').getSheetByName('Data');
  urls.forEach((u, i) => {
    const res = UrlFetchApp.fetch(u);                     // 待ちが積み上がる
    sh.getRange(i+1, 1).setValue(res.getContentText());  // 逐次書き込み
  });
}

// OK:並列fetch + 一括書き込み + 締切管理
function syncFast(urls) {
  const DEADLINE = Date.now() + 55_000;
  const ss = SpreadsheetApp.openById('ID');
  const sh = ss.getSheetByName('Data');

  const bodies = withRetry(() => fetchParallel(urls));   // 並列 + 再試行
  const values = bodies.map(b => [b.code, (b.body || '').slice(0, 500)]);

  if (Date.now() > DEADLINE) {
    // 次回に回す設計(省略可)
    scheduleNext(1);
    return;
  }
  sh.getRange(1, 1, values.length, values[0].length).setValues(values); // 一括
}

チェックリスト(上から順に)

□ 遅延区間を計測して特定したか(UrlFetch/Sheets/Drive など)
□ 外部リクエストは fetchAll + timeout + 再試行にしたか
□ スプレッドシート操作は getValues/setValues の一括・分割にしたか
□ 締切管理(自発停止→トリガー継続)を入れたか
□ onEdit/onOpen など短い実行枠に重処理を置いていないか
□ LockService で同時実行の競合を避けているか
□ キャッシュ/差分取得で無駄な処理を省いているか
□ Advanced Sheets API のバッチ更新に置き換えられる箇所はあるか