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 を最初に疑ってください。
エラーメッセージの全文:「起動時間の最大値を超えました」と「Exceeded maximum execution time」は同じ意味
同じタイムアウトでも、画面に出る文字列は Google アカウントの言語設定で変わります。どちらも「1回の実行に使える時間の上限に当たった」という同一の状態で、原因の系統も対処も同じです。
- 日本語表示:「起動時間の最大値を超えました」(実行数の画面やダイアログに、エラーの種類として並びます)
- 英語表示:「Exceeded maximum execution time」(公式の割り当て表では
Script runtimeの超過に当たります)
日本語の「起動時間」は、スクリプトが起動するまでの待ち時間ではなく実行が始まってから終わるまでの時間を指しています。ここを取り違えると「起動が遅いだけだろう」と読み流してしまい、本当の原因である処理量に手が付きません。英語表記のほうが実態に近い言い方です。
文字列が似ていても別のエラーである場合があります。たとえば 「server error: exception: 1日にサービス urlfetch を実行した回数が多すぎます」は実行時間ではなく呼び出し回数の日次クォータに当たったもので、分割実行を入れても直りません(切り分けは後述の表を使ってください)。そもそもエラーが1行も出ず、スクリプトが起動した気配すら無いなら、タイムアウトではなくトリガー側の問題です(GASのトリガーが動かないときの原因と確認手順)。
まず1分で確認する3点
コードを触る前に、データを壊さないための確認を先にします。
- 書き込み系の処理が途中で止まっていないか。タイムアウトは処理の途中で強制終了するため、「半分だけ書き込まれたシート」「重複して送られたメール」が残ることがあります。実行ログ(Apps Script エディタの「実行数」)で、どこまで進んだかを先に確認します。
- どの関数が落ちたか。実行数の画面で、落ちた関数名と実行の種類(手動 / トリガー)、所要時間を控えます。所要時間がきっかり上限に張り付いていればタイムアウト、それより短ければ別のエラーです。
- 同じ関数が多重に走っていないか。1分おきトリガーなどで、前の実行が終わる前に次が始まっていることがあります。この状態では1本あたりの処理も遅くなります。
この3点はどれも実行ログが読めることが前提です。実行数の画面に何も並ばない、あるいは console.log が1行も出ないときは、まずGASの実行ログが出ない場合の3系統の切り分けから手を付けます。ここまでで「落ちた関数」と「所要時間」が分かれば、あとは原因の特定に進めます。
公式の上限はいくつか(アカウント種別で違うのは合計時間のほう)
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を叩く自動化はこちらの枠も見ておきます。
「時間」の上限と「回数・日次クォータ」の上限は別枠
タイムアウトの相談でいちばん混ざるのが、この2つです。枠が別なので、片方を直してももう片方には効きません。出ているエラー文と、下の2つの表を突き合わせて切り分けてください(いずれも上記の公式割り当て表に記載の値。無料アカウントと Google Workspace で差があるものは分けてあります)。
表1:時間に関する上限
項目 | 無料アカウント(gmail.com 等) | Google Workspace |
|---|---|---|
Script runtime(1回の実行時間) | 6 min / execution | 6 min / execution |
Custom function runtime(セルに書く自作関数) | 30 sec / execution | 30 sec / execution |
Google Workspace add-on runtime | 30 sec / execution | 30 sec / execution |
Triggers total runtime(トリガーの1日合計) | 90 min / day | 6 hr / day |
Simultaneous executions per user(同時実行数) | 30 / user | 30 / user |
表2:回数・日次クォータ(時間とは無関係に先に当たる枠)
項目 | 無料アカウント(gmail.com 等) | Google Workspace |
|---|---|---|
URL Fetch calls(外部API呼び出し) | 20,000 / day | 100,000 / day |
Email recipients per day(MailApp 等の送信先数) | 100 / day | 1,500 / day |
Email read/write(送信を除く) | 20,000 / day | 50,000 / day |
Properties read/write | 50,000 / day | 500,000 / day |
Spreadsheets created(新規作成数) | 250 / day | 3,200 / day |
Triggers(トリガー数) | 20 / user / script | 20 / user / script |
「server error: exception: 1日にサービス urlfetch を実行した回数が多すぎます」は表2のほうです。実行時間はまだ余っていても、URL Fetch calls の日次枠を使い切ると出ます。この場合、4〜5分で打ち切る分割実行を入れても呼び出し回数は1回も減らないので直りません。対処は、fetchAll() でまとめる・取得結果を Cache Service に持つ・そもそも取りに行く行を絞る、のいずれかで回数そのものを減らすことです。メール送信が途中で止まる場合も同じで、Email recipients per day の枠を見ます。
逆に、所要時間がきっちり上限に張り付いて落ちているなら表1の話です。切り分けの順番としては、まず「エラー文に回数・クォータの語があるか」を見るのがいちばん早いです。
なお、この表の数値は Google 側で改定されることがあります。数字を根拠に設計を決めるときは、記事ではなく上記の公式ページを都度開いて確認してください。
先に見通しを1つ。下の5手順は「6分に収める」ための延命策です。処理量そのものが増え続けているなら、GASの外へ出す判断のほうが安くつくこともあります(AIシステム開発)。
原因別の対処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日合計時間が枠に迫っていて、処理を増やす余地がない。
- 作った本人しか直せず、その人が対応するたびに他の仕事が止まっている。
延命か置き換えかを、年間の手当て時間で計算する
「月1回以上」という目安をもう一段、数字に落とします。見るのは費用ではなく年間に奪われる時間です。使う変数は4つだけです。
- N:1回の実行で処理する件数(行数・レコード数)
- t:1件あたりの処理時間(秒)。一括読み書きに直した後の実測値を使います。
- D:分割実行を作り込む/手直しする工数(時間)。進捗保存・継続トリガー・トリガー削除の実装と検証まで。
- F × R:月に落ちる回数 × 1回の復旧にかかる時間。半分だけ書き込まれたシートの手直しや再実行の立ち会いも含めます。
年間の手当て時間は 年間手当て時間 = D + F × R × 12、1日に必要な処理時間は N × t 秒で出ます。この2本で判断します。
⚠ 以下の t・D・F・R の数値は、算式の動きを見せるための仮の係数です。実際の値は処理内容と運用体制で変わるので、必ず自分の環境で測った数字を入れ直してください。費用の相場としては使えません。
ケース | N(件) | t(秒/件・仮) | 1日の処理時間 N×t | D + F×R×12(仮) | 判断 |
|---|---|---|---|---|---|
小 | 800 | 0.3 | 240秒=4分 | 0 + 0 = 年0時間 | 一括読み書きで収まる。分割実行は作らない |
中 | 12,000 | 0.3 | 3,600秒=60分 | 6 + 0.5×1.5×12 = 年15時間 | 分割実行で延命が妥当。ただし個人アカウントの1日90分枠の67%を使う |
大 | 40,000 | 1.5 | 60,000秒=約1,000分 | 20 + 2×2×12 = 年68時間 | 時間分割では入らない。置き換え |
ここから引ける線は2本です。
- 年間手当て時間が20時間を超えたら、分割実行の作り込みではなく置き換えを検討する。20時間は「担当者が年に2〜3営業日を復旧作業に取られる」水準で、これを超えると翌年も同じだけ消える前提になります(上の表の「大」=年68時間は明確に超過、「中」=年15時間は範囲内)。
- 1日の処理時間 N×t が、トリガー1日合計枠の7割を超えたら時間分割では詰む。無料アカウントなら 90分 の7割=63分、Google Workspace なら 6時間 の7割=4.2時間です。残り3割は、再実行・手動実行・他のスクリプトのための余白として残します。
t に換算すると、1件あたり1.5秒かかる処理では無料アカウントで約2,500件/日、Workspace で約10,000件/日が時間分割の限界です(1件0.3秒なら順に約12,600件/日・約50,000件/日)。この線を越えている処理は、閾値をどう刻んでも1日の枠に収まりません。
なお「中」のケースのように枠の7割に近づいている場合、処理件数が1.5倍になった時点で線を越えます。いま入っているかではなく、1年後の件数で当てはめてください。
延命を続ける側を選ぶなら、落ちたときに誰が見るかを先に決めておくほうが安全です。保守として外に出す場合の考え方はGASの保守を月額で見てもらう場合の考え方に整理しています。
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回以上続くなら、直す対象はコードではなく仕組みのほう。
- 日本語の「起動時間の最大値を超えました」と英語の「Exceeded maximum execution time」は同じエラー。「1日にサービス urlfetch を実行した回数が多すぎます」は別枠(回数の日次クォータ)で、分割実行では直らない。
- 延命か置き換えかは年間手当て時間20時間とトリガー1日合計枠の7割(無料63分 / Workspace 4.2時間)の2本で線を引く。係数は仮なので実測値で計算し直す。
上限を避ける書き方で粘れる範囲と、処理の置き場所ごと変えたほうが早い範囲は、実際のスクリプトと件数を見ないと分かれません。どちらに当たるかの切り分けからお手伝いできます。
なお、実行そのものが始まらず「このアプリは確認されていません」で止まっている場合は、時間制限とは無関係の承認の話です。進め方と、社内配布なら警告を出さずに済ませる方法はGAS「このアプリは確認されていません」の消し方と進め方にまとめています。
よくある質問
GASの実行時間の上限は6分ですか?Google Workspaceなら伸びますか?
公式の割り当て表では、スクリプトの実行時間(Script runtime)は無料アカウント・Google Workspace のいずれも 6 min / execution と記載されており、アカウント種別による差はありません。種別で差があるのはトリガーの合計実行時間(Triggers total runtime)で、無料アカウントが 90 min / day、Google Workspace が 6 hr / day です。
「起動時間の最大値を超えました」と「Exceeded maximum execution time」は違うエラーですか?
同じエラーです。表示言語の違いだけで、どちらも1回の実行に使える時間の上限に当たった状態を指します。日本語の「起動時間」は起動までの待ち時間ではなく、実行が始まってから終わるまでの時間のことです。一方で「server error: exception: 1日にサービス urlfetch を実行した回数が多すぎます」は別枠のエラーで、実行時間ではなく URL Fetch calls の日次クォータに当たっています。
手動実行では通るのに、トリガーからだけタイムアウトするのはなぜですか?
1回あたりの6分ではなく、トリガーの1日あたり合計実行時間を使い切っている可能性があります。無料アカウントは1日90分、Google Workspace は1日6時間が上限です。1本ずつが6分以内でも、実行回数が多ければこの枠に先に当たります。
セルに書いた自作関数が30秒で止まります。分割すれば直りますか?
直りません。カスタム関数の実行時間の上限は 30 sec / execution で、通常の関数の6分とは別枠です。継続トリガーによる分割実行はカスタム関数では使えないため、カスタム関数をやめてメニューや時間主導トリガーから呼ぶ通常の関数に作り替える必要があります。
分割実行で粘るか、GAS以外に置き換えるかはどう判断すればよいですか?
記事内では「年間手当て時間=作り込み工数+月に落ちる回数×復旧時間×12」が年20時間を超えたら置き換えを検討する、1日の処理時間がトリガー1日合計枠の7割(無料63分/Workspace 4.2時間)を超えたら時間分割では収まらない、という2つの線を示しています。ただし記事中の1件あたり処理時間や復旧時間などの数値は算式の動きを見せるための仮の係数です。費用や相場の目安ではないため、必ず実測値を入れ直して計算してください。