Automation

Google Apps Script Maximum Execution Time (2026)

Marc SeanJuly 9, 20266 min read

For a script looping through 3,000 rows of actuals, calling SpreadsheetApp.getRange() on each one, and writing back derived values, 6 minutes is not a lot of runway.

The Limit in Full

As of July 2026, per Google's official Apps Script quotas documentation:

Account typeMax execution time per run
Google consumer (gmail.com)6 minutes
Google Workspace (paid plans)6 minutes
Time-based triggersSame as above
Event-driven triggers (onEdit, onOpen)Same as above

The limits apply identically whether you run the script manually, from a menu item, or from a scheduled trigger. There is no extended execution mode, and Google Support won't raise these limits via a quota increase request.

When a script hits the wall, the runtime throws Exception: Exceeded maximum execution time and terminates the execution immediately. Any work buffered in memory at that moment is lost.

The Three FP&A Patterns That Hit the Wall

Most one-off scripts run well under 60 seconds. The ones that die at 5:58 are doing one of these:

Cell-by-cell loops on large ranges. If your script reads a 2,400-row actuals export with getValue() per cell and writes back with setValue() per cell, you're making roughly 4,800 API roundtrips. Each call takes 50-200ms. At 150ms average: 4,800 Ă— 0.15s = 720 seconds. You time out at roughly 50% completion, every time.

Chained external API calls. A script pulling 18 months of revenue data from HubSpot or Stripe, parsing each month, and writing a summary to your P&L tab compounds every slow response. One API call returning in 3 seconds burns 8% of your consumer time budget before you've touched the sheet.

Multi-tab model refreshes done naively. Refreshing a 3-statement model - cascading from Assumptions to P&L to Cash Flow to Balance Sheet - touches hundreds of named ranges across multiple sheets. If the script flushes after each write, the overhead stacks fast on any model with realistic row counts.

Fix It in Two Steps Before You Reach for Chunking

Before implementing a continuation pattern, try the batch API first. It solves the problem for most models.

Step 1: Replace getValue()/setValue() loops with getValues()/setValues() on full ranges.

The bad pattern:

// Slow: 2,400 individual API calls
for (let i = 2; i <= 2401; i++) {
  const rev = sheet.getRange(i, 3).getValue();   // one call per row
  sheet.getRange(i, 10).setValue(rev * 0.385);   // one call per row
}

The correct pattern:

// Fast: 2 API calls for the same 2,400 rows
const data = sheet.getRange(2, 3, 2400, 1).getValues(); // one read
const output = data.map(([rev]) => [rev * 0.385]);
sheet.getRange(2, 10, 2400, 1).setValues(output);       // one write
SpreadsheetApp.flush();

A single getValues() call on 2,400 rows costs roughly the same wall-clock time as a single getValue() on 1 cell. The speedup on a 2,400-row table is typically 40-60x. Most models don't need the chunking pattern - they need this.

Step 2: If the job still runs long, implement a continuation with PropertiesService.

The Continuation Pattern for Large Models

When batch API isn't enough (50,000-row export tables, slow external calls you can't batch), you need to checkpoint progress across executions.

function refreshActuals() {
  const CHUNK_SIZE = 250; // rows per execution - tune based on complexity
  const props = PropertiesService.getScriptProperties();

  // Resume from last checkpoint, or start at row 2 (row 1 = headers)
  const startRow = parseInt(props.getProperty('lastRow') || '2');

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const src = ss.getSheetByName('Actuals');
  const dest = ss.getSheetByName('P&L');
  const lastRow = src.getLastRow();
  const endRow = Math.min(startRow + CHUNK_SIZE - 1, lastRow);

  // One read for the entire chunk
  const rows = src.getRange(startRow, 1, endRow - startRow + 1, 8).getValues();

  // Transform: revenue in col 3 (index 2), COGS in col 4 (index 3)
  // Writing gross profit and gross margin to P&L cols J and K
  const output = rows.map(r => {
    const rev = r[2];
    const cogs = r[3];
    const gp = rev - cogs;
    const gm = rev > 0 ? gp / rev : 0;
    return [gp, gm];
  });

  // One write for the entire chunk
  dest.getRange(startRow, 10, output.length, 2).setValues(output);
  SpreadsheetApp.flush();

  if (endRow < lastRow) {
    // Save progress and schedule next execution
    props.setProperty('lastRow', String(endRow + 1));
    ScriptApp.newTrigger('refreshActuals')
      .timeBased()
      .after(1500) // 1.5-second gap between executions
      .create();
  } else {
    // Job complete - clear checkpoint and clean up triggers
    props.deleteProperty('lastRow');
    deleteOldTriggers('refreshActuals');
  }
}

function deleteOldTriggers(fnName) {
  ScriptApp.getProjectTriggers()
    .filter(t => t.getHandlerFunction() === fnName)
    .forEach(t => ScriptApp.deleteTrigger(t));
}

Call deleteOldTriggers() at the start of each execution too, not just the end. Without that, each run creates a trigger and you'll accumulate stale ones fast. Apps Script hard-caps time-based triggers at 20 per user per script - you'll hit that limit on execution 20 of a 50-chunk job if you're not cleaning up. See Apps Script Quotas: Official Limits (2026) for the full quota table.

Why Workspace Still Doesn't Solve Long-Running Jobs

Upgrading to Google Workspace does not extend the 6-minute per-execution runtime. Workspace provides a larger daily trigger-runtime allowance, but each individual execution still needs to finish or checkpoint before the 6-minute wall.

As of July 2026, Google's quota table lists 6 hours per day of total trigger runtime for Workspace accounts and 90 minutes per day for consumer accounts. This aggregate quota is separate from the 6-minute per-execution limit.

If a quarterly board pack refresh runs for 8 minutes per execution via a trigger, and you're running it every 30 minutes during business hours (8 hours Ă— 2 per hour = 16 executions Ă— 8 min = 128 minutes), you burn through the consumer daily budget before noon. Workspace gives you more room, but high-frequency refresh automation on large models still requires you to think about both the per-execution and daily totals together.

The Apps Script 6-Minute Execution Limit: Official Quota article covers these daily ceilings in more detail.

When the Architecture Is the Problem

Some jobs don't fit the execution time model at all, regardless of chunk size:

  • Refresh cadences shorter than 5 minutes, running 24/7
  • Pulling 100,000+ rows from a rate-limited external API
  • Transformations that need to run sequentially across 20+ tabs before writing any output

For these, Apps Script is the wrong execution environment. The script is trying to do backend work - the kind of work that belongs in a server process with no time cap, not in a sandboxed cloud function with a 6-minute fuse.

ModelMonkey handles data pulls and transformations server-side, so the 6-minute cap isn't a factor. The result lands in your sheet when it's ready, without holding an Apps Script execution open. If you're hitting the execution time limit on scheduled refreshes or multi-source board pack automation, the issue is often architectural - the right fix isn't a bigger chunk size.

Try ModelMonkey free for 14 days - it works in both Google Sheets and Excel.


Frequently Asked Questions