Google Apps Script(GAS)で作った自動化が、ある日突然 Exceeded maximum execution time(最大実行時間を超えました)で止まる。昨日まで動いていたのに、行が増えただけで落ちる——スプレッドシート運用を GAS で支えている現場で、いちばんよく起きるトラブルです。
このエラーは「スクリプトが壊れた」わけではありません。1回の実行に使える時間の上限を、処理が超えてしまったというだけの話です。だから直し方も、原因の系統ごとにほぼ決まっています。この記事では公式ドキュメントの数値に当たりながら、切り分けの順番と恒久対策を整理します。
結論:実行時間の超過は「上限」「呼び出し回数」「データ量」の3系統
原因は無数にあるように見えて、実際には次の3系統に束ねられます。上から順に潰していくのが最短です。
- 系統A:上限そのものに当たっている — 1回の実行時間や、トリガーの1日あたり合計時間という公式の上限に到達している。
- 系統B:呼び出し回数が多すぎる — スプレッドシートを1セルずつ読み書きしている、ループの中で毎回
getRange()やUrlFetchAppを叩いている。実行時間超過の大半はここです。 - 系統C:データ量が増えて線形に伸びた — ロジックは同じなのに、行数が2倍になって時間も2倍になり、上限をまたいだ。
「昨日まで動いていた」なら C、「作った直後から重い」なら B、「時間帯によって落ちたり落ちなかったりする」なら A を最初に疑ってください。
まず1分で確認する3点
コードを触る前に、データを壊さないための確認を先にします。
- 書き込み系の処理が途中で止まっていないか。タイムアウトは処理の途中で強制終了するため、「半分だけ書き込まれたシート」「重複して送られたメール」が残ることがあります。実行ログ(Apps Script エディタの「実行数」)で、どこまで進んだかを先に確認します。
- どの関数が落ちたか。実行数の画面で、落ちた関数名と実行の種類(手動 / トリガー)、所要時間を控えます。所要時間がきっかり上限に張り付いていればタイムアウト、それより短ければ別のエラーです。
- 同じ関数が多重に走っていないか。1分おきトリガーなどで、前の実行が終わる前に次が始まっていることがあります。この状態では1本あたりの処理も遅くなります。
ここまでで「落ちた関数」と「所要時間」が分かれば、あとは原因の特定に進めます。
公式の上限はいくつか(アカウント種別で違うのは合計時間のほう)
Google の公式ドキュメントには、Apps Script の上限が表で公開されています。実行時間に関わる主な数値は次のとおりです(Quotas for Google Services|Google Apps Script 公式ドキュメント)。
- Script runtime(スクリプトの実行時間):6 min / execution — 無料の個人アカウントでも Google Workspace アカウントでも同じ6分です。「Workspace なら30分使える」という話をときどき見かけますが、公式の表は両方とも 6 min / execution と記載されています。
- Custom function runtime(カスタム関数の実行時間):30 sec / execution — シートのセルに
=MYFUNC()と書いて呼ぶ自作関数は、6分ではなく30秒です。ここを混同していると「同じコードなのにセルからだと落ちる」が起きます。 - Triggers total runtime(トリガーの合計実行時間):個人アカウント 90 min / day、Google Workspace 6 hr / day — アカウント種別で差が出るのはこちらです。1本ずつは6分以内でも、1日の合計がこの枠を超えると以降のトリガーが動かなくなります。
- Simultaneous executions per user(同時実行数):30 / user。
- Triggers(トリガー数):20 / user / script。
- URL Fetch calls:個人 20,000 / day、Workspace 100,000 / day — 外部APIを叩く自動化はこちらの枠も見ておきます。
なお、この表の数値は Google 側で改定されることがあります。数字を根拠に設計を決めるときは、記事ではなく上記の公式ページを都度開いて確認してください。
原因別の対処5手順
手順1:まず「上限に当たっているのか」を確定させる
落ちた関数の先頭で開始時刻を取り、要所で経過時間をログに出します。
const start = Date.now(); …処理… console.log((Date.now() - start) / 1000 + '秒');
これで「5分50秒あたりで毎回落ちる」なら、実行時間の上限に当たっているのが確定します。あわせて、トリガー実行だけが落ちるのか、手動実行でも落ちるのかを分けて見ます。手動では通るのにトリガーだけ止まる場合は、1回の6分ではなく1日の合計実行時間(90分 / 6時間)を使い切っている可能性が高いです。
ここで直らない(=所要時間が上限よりずっと短いところで落ちている)なら、原因は実行時間ではありません。権限エラーやAPIのエラーを疑ってください。
手順2:1セルずつの read / write を一括読み書きに置き換える
実行時間超過でいちばん多いのがこれです。公式のベストプラクティスは、読み書きを交互に繰り返すことを明確に「遅い」としており、「すべてのデータを1回の命令で配列に読み込み、配列上で処理し、1回の命令で書き出す」ことを推奨しています。同ページの例では、セルごとに処理した場合の約70秒が、一括処理では約1秒になったと示されています(Best Practices|Google Apps Script 公式ドキュメント)。
具体的には、ループの中の getRange(i, 1).getValue() / setValue() を消します。
- 読み込みは
const values = sheet.getDataRange().getValues();で二次元配列に一括取得する。 - 加工はすべて JavaScript の配列操作で行い、その間はシートに触らない。
- 書き戻しは
sheet.getRange(2, 1, values.length, values[0].length).setValues(values);で一括。 - 背景色や書式も同じで、
setBackgroundColor()の繰り返しではなくsetBackgrounds(colors)にまとめる。 - ループの中で
SpreadsheetApp.flush()を呼んでいる場合は外す(毎回サーバーと往復するため極端に遅くなります)。
1,000行を1セルずつ触っていた処理が、この置き換えだけで数十秒から1〜2秒になるのは珍しくありません。ここで所要時間が桁で縮まらないなら、ボトルネックはシートの読み書きではなく、外部サービスの呼び出しかデータ量です。
手順3:ループの中の「毎回やらなくていい処理」を外へ出す
公式も「Apps Script の中で完結する処理は、Google のサーバーや外部サーバーからデータを取ってくるより速い」と明記しています。ループの中に紛れている次のような処理を、ループの外へ移します。
SpreadsheetApp.getActiveSpreadsheet()/getSheetByName()の再取得。1回だけ取って変数に持つ。- 行ごとの
UrlFetchApp.fetch()。まとめて取れるAPIならfetchAll()を使うか、取得結果を Cache Service に入れて使い回す。公式のベストプラクティスでも、繰り返し取得するデータのキャッシュが推奨されています。 - 行ごとの
GmailApp/DriveApp呼び出し。検索条件をまとめ、取得は1回にする。 - ループ内の
Utilities.sleep()。100行に1秒入れれば、それだけで100秒消えます。
ここまでで6分を切らないなら、処理そのものが1回の実行に収まらない量になっています。手順4へ進みます。
手順4:バッチ処理+継続トリガーで分割実行にする
データ量が増えて線形に伸びたケースは、速くするのではなく「途中で中断して、続きから再開する」形に作り替えるのが正攻法です。手順は次のとおりです。
- 進捗を記録する場所を決める。
PropertiesService.getScriptProperties()に「次に処理を始める行番号」を保存します(シートの管理用セルでも可)。 - 関数の冒頭で開始時刻を取る。
const start = Date.now(); - ループの中で残り時間を見る。
if (Date.now() - start > 4.5 * 60 * 1000) { ... }のように、6分ではなく4〜5分で自主的に打ち切る閾値を置きます。上限ぎりぎりを狙うと、書き込みの途中で強制終了されます。 - 打ち切るときに、次の開始位置を保存して継続トリガーを作る。
ScriptApp.newTrigger('main').timeBased().after(60 * 1000).create();で「1分後に自分をもう一度呼ぶ」トリガーを立ててreturnします。after(durationMilliseconds)は「現在時刻から最短でどれだけ後に実行するか」を指定するメソッドとして公式リファレンスに定義されています(ClockTriggerBuilder|Google Apps Script リファレンス)。 - 最後まで終わったら、進捗をリセットし、自分が作った継続トリガーを削除する。
ScriptApp.getProjectTriggers()で走査してdeleteTrigger()します。ここを忘れるとトリガーが溜まり、上限(20 / user / script)に当たって新しいトリガーが作れなくなります。
この形にすると、1回の実行は必ず5分以内で終わり、処理量が増えても実行回数が増えるだけになります。ただしトリガーの1日合計(個人90分 / Workspace 6時間)は消費し続けるので、無限に伸ばせるわけではありません。分割したのに翌日から動かなくなった、という場合はこの枠を見てください。
手順5:重い集計はシート側の関数に寄せる
GAS で行を回して集計している処理は、そもそもスプレッドシートの関数にやらせたほうが速いことが多いです。
- 合計・件数・平均 →
QUERY/SUMIFS/COUNTIFSを集計用シートに置き、GAS は結果の範囲を1回読むだけにする。 - 突き合わせ → 行ごとの検索ループをやめ、
VLOOKUP/XLOOKUPの列を作って一括でgetValues()。 - 絞り込み → GAS 側の
filterの前にFILTER関数で対象行を減らしておく。
ただしこの寄せ方は、シート側が重くなるとかえって遅くなります。関数が増えすぎてシート自体が開かない状態なら、先にスプレッドシートが重い・開かない原因と軽くする対処を片付けてください。シートの再計算が遅いままだと、GAS を分割しても改善しません。
なお、カスタム関数(セルに書く自作関数)で重い処理をしている場合は、30秒の壁は手順4の分割では回避できません。カスタム関数をやめ、メニューやトリガーから実行する通常の関数に作り替えるのが唯一の道です。
毎週のように落ちるなら、GASの延命ではなく置き換えの時期
ここまでの手順は、ほとんどの「実行時間を超過しました」を実際に止められます。ただし、注意しておきたい傾向があります。
一括読み書きに直し、分割実行を入れ、シート側に寄せて——それでも半年後にまた落ちる。落ちるたびに閾値を下げ、トリガーを足し、進捗管理のコードが増えていく。この状態になっているとき、起きているのは不具合ではなく「データ量と業務が、スプレッドシート+GAS の器を超えた」という設計の話です。
判断の目安は次のあたりです。
- タイムアウト対応が月1回以上発生している。
- 分割実行のための進捗管理コードが、本来やりたい処理より長くなっている。
- トリガーの1日合計時間が枠に迫っていて、処理を増やす余地がない。
- 作った本人しか直せず、その人が対応するたびに他の仕事が止まっている。
GAS の改修を外注している場合は、延命1回あたりの費用も並べてみてください。目安はGAS開発を外注する場合の費用相場にまとめています。年に数回の改修費が積み上がっているなら、作り替えたほうが安く済む領域に入っていることがあります。
正直なところ、GAS で十分な業務のほうが多いです。メールの自動送信のような定型処理は、GASでスプレッドシートからメールを自動送信する方法の範囲で長く安定して動きます。作り替えを考えるべきなのは、データ量が右肩上がりで、しかも止まると業務が止まる処理だけ。どちらなのか切り分けたい段階でも、現在のスクリプトと処理量を見れば判断はできます。
スプレッドシートでの管理そのものをどこまで続けるかについては、スプレッドシート管理の限界と、アプリ化を考える基準もあわせてご覧ください。
まとめ
- 1回の実行時間の上限は公式表で 6 min / execution。個人アカウントと Google Workspace で差はない。
- 差が出るのはトリガーの1日合計(個人 90 min / day、Workspace 6 hr / day)。トリガーだけ止まるならこちらを疑う。
- セルに書くカスタム関数は30秒。分割実行では回避できない。
- まず
getValues()/setValues()の一括読み書きに直す。ここで大半が解決する。 - それでも収まらなければ、4〜5分で自主的に打ち切り、
after()の継続トリガーで続きから再開する形に作り替える。終了時のトリガー削除を忘れない。 - タイムアウト対応が月1回以上続くなら、直す対象はコードではなく仕組みのほう。
よくある質問
GASの実行時間の上限は6分ですか?Google Workspaceなら伸びますか?
公式の割り当て表では、スクリプトの実行時間は個人アカウント・Google Workspace のいずれも 6 min / execution と記載されており、アカウント種別による差はありません。種別で差があるのはトリガーの合計実行時間で、個人アカウントが 90 min / day、Google Workspace が 6 hr / day です。
手動実行では通るのに、トリガーからだけタイムアウトするのはなぜですか?
1回あたりの6分ではなく、トリガーの1日あたり合計実行時間を使い切っている可能性があります。個人アカウントは1日90分、Google Workspace は1日6時間が上限です。1本ずつが6分以内でも、実行回数が多ければこの枠に先に当たります。
セルに書いた自作関数が30秒で止まります。分割すれば直りますか?
直りません。カスタム関数の実行時間の上限は 30 sec / execution で、通常の関数の6分とは別枠です。継続トリガーによる分割実行はカスタム関数では使えないため、カスタム関数をやめてメニューや時間主導トリガーから呼ぶ通常の関数に作り替える必要があります。
一括読み書きに直すと、どれくらい速くなりますか?
公式のベストプラクティスでは、セルごとに読み書きを繰り返す処理に約70秒かかったものが、配列への一括読み込みと一括書き出しに変えて約1秒になった例が示されています。実際の倍率は処理内容によりますが、ループ内の getValue / setValue を getValues / setValues に置き換えるのが最初に効く対処です。
継続トリガーを使った分割実行で気をつけることは?
打ち切りの閾値を6分ぎりぎりではなく4〜5分に置くこと、次の開始位置を PropertiesService などに保存すること、処理が全部終わったら自分で作ったトリガーを deleteTrigger で消すことの3点です。削除を忘れるとトリガーが溜まり、上限の 20 / user / script に当たって新規作成ができなくなります。