Fix Google Apps Script Exceeded Maximum Execution Time

Google Apps Script’s “Exceeded maximum execution time” error fires when a script surpasses Google’s hard wall-clock limit (6 minutes for free accounts, 30 minutes for Workspace). This almost always happens because you are calling getValue() or setValue() inside a for loop, incurring a network round-trip for every single row.

To fix this instantly, replace all per-row read/write calls with a single batched array memory map:

JavaScript

function applyPricing() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const lastRow = sheet.getLastRow();

  // ONE network read: pulls all data into a local 2D array
  const inputData = sheet.getRange(2, 3, lastRow - 1, 2).getValues();

  // All processing runs instantly in V8 memory — zero network calls
  const outputData = inputData.map(([units, price]) => {
    const finalPrice = units > 200 ? price * units * 0.88 : price * units;
    const status = units > 200 ? 'DISCOUNTED' : 'STANDARD';
    return [finalPrice, status];
  });

  // ONE network write: pushes the entire result set back in a single operation
  sheet.getRange(2, 5, outputData.length, 2).setValues(outputData);
}

Collapsing thousands of read/write operations into one getValues() and one setValues() eliminates network latency. A script that previously timed out processing 15,000 rows in 6 minutes will now complete in under 5 seconds.

Prerequisites

  • Google Apps Script V8 Runtime: Ensure V8 is active under Project Settings > Runtime version > V8. The legacy Rhino runtime handles array mapping differently.
  • Contiguous Data Ranges: The getRange().getValues() method reads a rectangular block. If your data is spread across non-adjacent columns, you must read the whole block and select the specific array indices in memory.

The Syntax Breakdown

This fix operates across four mechanistic stages, shifting the workload from Google’s servers to your local memory:

  • sheet.getRange(2, 3, lastRow - 1, 2) — Constructs a exact boundary box. Specifying exact row/column dimensions prevents getDataRange() from needlessly pulling thousands of blank columns into memory.
  • .getValues() — Executes a single Spreadsheet Service API call. The returned 2D array lives entirely in V8 memory for the rest of the execution.
  • .map(([units, price]) => [...]) — Destructures each sub-array from the input array, applies your business logic in memory, and returns a new 2D array. No further service calls occur during this loop.
  • .setValues(outputData) — Executes exactly one write call, pushing the calculated 2D output array directly back onto the sheet grid.

Real-World Example: E-Commerce Nightly Order Pricing

An operations manager at a mid-size e-commerce retailer runs a nightly Apps Script to apply volume discounts to 15,000 order rows. The script was authored when the company only had 2,000 orders. Because it uses a traditional for loop with setValue(), it makes 30,000 network calls per run.

At the current data volume, it hits the 6-minute ceiling and throws “Exceeded maximum execution time” consistently around row 4,200—leaving 70% of the daily reporting blank.

Raw Data Table (Columns C–D are inputs, E–F are outputs)

Order IDUnits SoldUnit PriceFinal PriceStatus
ORD-100112045.00(blank)(blank)
ORD-100234022.50(blank)(blank)
ORD-10037867.00(blank)(blank)

The Refactored Execution

The manager replaces the loop with the getValues() memory map.

Output Table

Order IDUnits SoldUnit PriceFinal PriceStatus
ORD-100112045.005,400.00STANDARD
ORD-100234022.506,732.00DISCOUNTED
ORD-10037867.005,226.00STANDARD

All 15,000 rows now calculate and print to the sheet in under 10 seconds.

Advanced: The PropertiesService Continuation Pattern

If you are processing hundreds of thousands of rows and hitting the timeout even with array batching, you must build a state-saving continuation trigger.

This code stops the script at the 5-minute mark, saves its exact row position, and tells Google to run the script again 2 seconds later starting from that exact spot:

JavaScript

function applyPricingContinuation() {
  const MAX_MS = 5 * 60 * 1000; // 5-min budget
  const start = Date.now();
  const props = PropertiesService.getScriptProperties();
  
  // Clear old triggers to prevent quota burn
  ScriptApp.getProjectTriggers().forEach(t => {
    if(t.getHandlerFunction() === 'applyPricingContinuation') ScriptApp.deleteTrigger(t);
  });

  let startIdx = parseInt(props.getProperty('resumeRow') || '0');
  
  for (let i = startIdx; i < massiveArray.length; i++) {
    if (Date.now() - start > MAX_MS) {
      // Save position and schedule next run
      props.setProperty('resumeRow', i.toString());
      ScriptApp.newTrigger('applyPricingContinuation').timeBased().after(2000).create();
      return; 
    }
    // Processing logic here...
  }
  props.deleteProperty('resumeRow'); // Clear state when finished
}

Common Related Errors & Fixes

  • Script Times Out Mid-Loop with No Error Indicator -> Cause: You are using setValue() incrementally. When the 6-minute timeout hits, the script dies instantly without rolling back written values, leaving a partially updated sheet. -> Fix: The batch setValues() pattern resolves this atomically. The entire output array computes in memory before a single cell is written, so either all 15,000 rows write, or none do.
  • Orphaned Time-Based Triggers Accumulate -> Cause: You implemented the continuation pattern, but didn’t delete the previous trigger before creating the next one. This burns your daily execution quota rapidly. -> Fix: Always include a trigger deletion loop at the start of your continuation function (as shown in the advanced script above).

Key Takeaways

  • The primary fix is replacing per-row setValue() loops with a single batched setValues() array write.
  • Execution time limits apply to wall-clock elapsed time (including network wait times), not CPU processing time.
  • For datasets exceeding 100,000 rows, implement a PropertiesService checkpoint loop to bypass the ceiling entirely.

Frequently Asked Questions

What is the exact execution time limit for Google Apps Script?

Consumer Google accounts (free Gmail) have a hard limit of 6 minutes per script execution. Google Workspace accounts (Business Starter, Standard, Plus, and Enterprise) have a ceiling of 30 minutes per execution.

Does calling SpreadsheetApp.flush() inside a loop fix timeouts?

No, it makes them worse. SpreadsheetApp.flush() forces all queued write operations to execute immediately against the Google server. This converts Apps Script’s internal write queue into a synchronous blocking call, massively increasing network latency.

Can I buy more execution time in Google Cloud Console?

No. Google does not expose a per-project execution time setting. You cannot extend the limit through script configurations or GCP billing. Your only options are upgrading to a Workspace account or utilizing the continuation pattern.

About the Architect: Grid and Formula

Lead Data Analyst and Workspace Automation Engineer at Grid & Formula. Specializing in Excel logic, Google Sheets architecture, and Notion database blueprints. I build and rigorously test the formula frameworks and error fixes published here, ensuring you get immediate, no-fluff solutions to your most complex data bottlenecks.