Automation

Google Apps Script Limitations (2026 Complete Guide)

Marc SeanJuly 11, 20267 min read

Apps Script Limits at a Glance

Before the detail, here's the reference table:

LimitValueMost Likely to Hit
Execution time (consumer)6 minutesBulk updates, large exports
Execution time (consumer and Workspace)6 minutesComplex automation
Memory (heap)~100MBLarge data pulls, JSON parsing
UrlFetch response50MBExternal API payloads
PropertiesService total500KBPersistent state storage
PropertiesService per key9KBLong config strings
Script file size50MBEmbedded assets
Simultaneous executions30Concurrent triggers

Execution Time Limitations: The 6-Minute Wall

Six minutes sounds like enough until you're running a month-end close that needs to read 8 tabs, aggregate to a summary, and push the result to a reporting sheet. At 100-500ms per Sheets API call, you can burn through your budget fast.

The underlying issue is that Apps Script is synchronous and single-threaded. Every SpreadsheetApp.getActiveSpreadsheet() call, every sheet.getRange(), every Utilities.sleep() - they all run in series. There's no async/await, no parallelism.

A 50,000-row GL export pulling from three source sheets will time out. Not might. Will.

The standard fix is chunking with continuation tokens stored in PropertiesService:

function processLargeExport() {
  const props = PropertiesService.getScriptProperties();
  // Pick up where we left off if the previous run timed out
  let startRow = parseInt(props.getProperty('lastProcessedRow') || '2');
  
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
                               .getSheetByName('GL Export');
  const data = sheet.getRange(startRow, 1, 500, 8).getValues(); // 500-row chunks
  
  // Process chunk...
  
  // Save progress before timeout hits
  props.setProperty('lastProcessedRow', String(startRow + 500));
  
  // Trigger the next run if there's more data
  if (startRow + 500 < sheet.getLastRow()) {
    ScriptApp.newTrigger('processLargeExport')
             .timeBased()
             .after(10000) // 10 seconds
             .create();
  } else {
    props.deleteProperty('lastProcessedRow'); // Done
  }
}

This works, but it adds complexity and it's brittle. Trigger creation has its own quota (20 user triggers per script), and Google's documentation notes that triggers may be delayed by several minutes under load. You're not getting real-time processing here.

The Synchronous Bottleneck: Why Batching Matters

Single-threaded execution means every round-trip to the Sheets API adds up. The number I've seen most often cited is 100-500ms per call under normal conditions, but it can spike higher under quota pressure.

The fix is batching. Instead of writing cell-by-cell inside a loop:

// Slow - one API call per row (hits execution limits fast on large ranges)
for (let i = 0; i < deals.length; i++) {
  sheet.getRange(i + 2, 5).setValue(deals[i].arr * 
    'Returns Analysis'!$C$4); // This doesn't even work cross-tab
}

You build the array first and write once:

// Fast - single API call regardless of row count
const outputData = deals.map(deal => [
  deal.name,
  deal.arr,
  deal.arr * multiplier,  // pre-fetch multiplier from Assumptions tab first
  deal.closeDate
]);
sheet.getRange(2, 1, outputData.length, 4).setValues(outputData);

The batching principle applies to reads too. One getValues() on 'P&L'!B2:F200 beats 100 individual getValue() calls by an order of magnitude.

Memory and Storage Limitations in Apps Script

The ~100MB heap limit sounds large. It isn't when you're parsing a JSON response from an ERP system or building an in-memory representation of a complex model.

The practical ceiling is lower than 100MB in most cases - garbage collection is aggressive and unpredictable. If you're fetching a large dataset from an external API, parsing it to JSON, and then writing it to Sheets, you can hit memory errors on payloads that look safely under the limit on paper.

PropertiesService is the other storage wall. At 500KB total and 9KB per key, it's not a database. It's a config store. Trying to cache API responses or model state in Properties is a common mistake that works fine for small datasets and silently breaks once you scale up.

// This pattern breaks above ~9KB per stored value
const props = PropertiesService.getScriptProperties();
props.setProperty('cachedRates', JSON.stringify(largeRatesObject)); // Fails silently if >9KB

If you need more persistent storage, the real alternatives are hidden sheet tabs (which have their own quirks with named ranges and formula references) or external storage like Firestore - which brings us to the network problem.

Network and External Access Limitations

Apps Script can't reach private network addresses. No VPN tunnels, no on-prem database connections, no intranet endpoints. UrlFetch is limited to public URLs, and the response cap is 50MB - which sounds fine until you're pulling a large dataset from a public API.

There's also a daily UrlFetch quota. As of 2025, Google's documentation lists 20,000 calls per day for consumer accounts and higher limits for Workspace accounts - but these are shared across all scripts running under your account. If you have multiple automations hitting external APIs, they compete for the same pool.

For financial models that need to pull from Bloomberg, an internal data warehouse, or an ERP system sitting behind a firewall, this is a hard wall. Apps Script simply can't bridge to those systems directly.

When Apps Script Isn't the Right Tool

The execution and memory limitations point to a clear pattern: Apps Script works well for orchestration (triggering, routing, light data transformation) and poorly for heavy computation.

A quarterly board pack refresh that pulls from 'P&L', 'Balance Sheet', and 'Cash Flow' tabs, calculates FCFF, and formats an output summary can usually be done inside Apps Script if it stays under ~5,000 rows and doesn't require external calls. A full sensitivity analysis across 200 scenarios, or anything that needs to call an external API for each row, will fight the runtime constantly.

The architectural decision that matters is where the computation lives. Apps Script as a thin client that coordinates reads and writes, with the actual processing happening server-side or in formulas, is more reliable than Apps Script trying to do everything.

This is the approach ModelMonkey takes - it uses Apps Script as a bridge to a backend service rather than trying to process large financial datasets inside the scripting environment. When you ask it to build a three-statement model or run a WACC calculation across your assumptions, the heavy computation doesn't happen in Apps Script's 6-minute window. It happens server-side and writes the result back. That's why it can handle things that would time out in a custom script.

Limits You Can Work Around vs. Limits You Can't

In summary, here's how to think about the App Script limitations you'll actually hit:

Workable with effort: Execution time (chunking + triggers), synchronous bottlenecks (batching), PropertiesService storage (use sparingly, offload large state to sheets).

Harder to work around: Memory ceiling on large datasets (redesign to stream or chunk the data), no private network access (you need a relay service or public endpoint), daily quotas on external calls (rate-limit your automation and build in quota awareness).

The 6-minute wall is the most common complaint, but it's usually a symptom of unbatched API calls rather than a fundamentally unsolvable problem. Fix the batching first. If you're still hitting the limit after batching, you've outgrown what Apps Script can do in a single execution and you need the chunking pattern above.

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


Frequently Asked Questions