That's the short version. The longer version matters if you're trying to automate anything meaningful in a multi-tab financial model.
The Official Quota Numbers
According to Google's Apps Script quotas documentation (verified July 2026):
| Account type | Max execution time per run |
|---|---|
| Consumer (free @gmail.com) | 6 minutes |
| Google Workspace (paid) | 6 minutes |
Time-based triggers are subject to separate daily caps:
| Account type | Total daily trigger runtime |
|---|---|
| Consumer | 90 minutes/day |
| Google Workspace | 6 hours/day |
There's also a cap of 30 simultaneous executions per script, and the Sheets API quota of 300 write requests per minute per project can force Utilities.sleep() calls that eat into your execution window before you've accomplished anything.
Workspace's larger daily trigger-runtime allowance does not extend an individual run. A script that needs more than 6 minutes still needs batching and checkpoints.
Why FP&A Models Hit This Wall
A simple =SUMIFS or =INDEX/MATCH formula doesn't use Apps Script at all. The problem starts when you need to do things formulas can't:
Data ingestion loops. Pulling actuals from a REST endpoint row by row, matching against your Assumptions! tab, and writing results back. A 24-month model with 40 line items is 960 individual Sheets API calls minimum. At typical latency, you're looking at 3-4 minutes for that alone - before any calculation logic.
Cross-tab reconciliation. Scripts that read from P&L, Balance Sheet, and Cash Flow simultaneously, check that retained earnings ties, and flag discrepancies. Reading ranges across multiple tabs, doing the comparison, and writing a status column back adds up fast.
Board pack formatting. Looping through 12 monthly columns, applying conditional formatting, hiding zero rows, setting print areas, and generating PDFs. The PDF generation step alone - using DriveApp.createFile() - can consume 45-90 seconds per sheet.
IMPORTDATA refresh workarounds. Because IMPORTDATA and IMPORTXML have their own refresh problems (see Auto Update Google Sheets), some analysts use Scripts to delete and re-enter the formula to force a refresh. Doing this across 15 cells with a sleep between each one chews through the budget.
What Actually Kills Your Script (It's Not What You Think)
The obvious culprit is loop count. The less obvious one is SpreadsheetApp.flush().
Every getValue() or setValue() call inside a loop triggers a round-trip to Google's servers. A script doing this:
// Slow: 500 API round-trips
for (let i = 2; i <= 501; i++) {
let val = sheet.getRange(i, 3).getValue(); // 1 API call
sheet.getRange(i, 4).setValue(val * 1.085); // 1 API call
}
...makes 1,000 API calls for 500 rows. At ~30ms per call in ideal conditions, that's 30 seconds just in I/O. Add network jitter, quota backoff, and any Utilities.sleep() calls to avoid rate limits, and you can hit the wall at 300 rows.
The fix is batching:
// Fast: 2 API calls total
const data = sheet.getRange(2, 3, 500, 1).getValues(); // read all at once
// Apply growth rate in memory - no API calls in the loop
const updated = data.map(row => [row[0] * 1.085]);
sheet.getRange(2, 4, 500, 1).setValues(updated); // write all at once
This processes the same 500 rows in roughly 200ms of I/O. The difference is 30 seconds vs 0.2 seconds - that's not a micro-optimization, it's whether your quarterly refresh completes at all.
The Continuation Pattern: Running Past 6 Minutes
When batching still isn't enough - when you're processing 5,000 rows or running multi-step operations across tabs - the standard approach is to save state and restart.
Apps Script provides PropertiesService.getScriptProperties() as persistent storage that survives between executions. You checkpoint your progress, kill the current run before the timer does it for you, and schedule the next chunk.
function processActualsWithContinuation() {
const props = PropertiesService.getScriptProperties();
const startTime = Date.now();
const MAX_RUNTIME_MS = 5 * 60 * 1000; // 5 min - stop before the 6-min wall hits
// Resume from where we left off, or start at row 2
let currentRow = parseInt(props.getProperty('lastRow') || '2');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Actuals');
const lastRow = sheet.getLastRow();
while (currentRow <= lastRow) {
// Check time budget every 100 rows
if (currentRow % 100 === 0 && (Date.now() - startTime) > MAX_RUNTIME_MS) {
props.setProperty('lastRow', currentRow.toString()); // save progress
// Schedule ourselves to run again in 1 minute
ScriptApp.newTrigger('processActualsWithContinuation')
.timeBased()
.after(60 * 1000)
.create();
return; // graceful exit
}
// Your processing logic here
// ...
currentRow++;
}
// Finished - clear state and remove any pending triggers
props.deleteProperty('lastRow');
}
This works, but it has real costs: latency between chunks, trigger accumulation if you're not cleaning up, and debugging complexity when something fails mid-continuation. It's the right answer when you need it, but it's not a pleasant development experience.
Other Quotas That Bite Without Warning
The 6-minute limit is the most famous, but these catch people in production:
Spreadsheet read/write rate: 300 requests/minute per project. If your script calls getValues() in rapid succession across tabs, you'll hit this and need Utilities.sleep(200) between calls - which then compounds your execution time problem.
Email quota: 100 emails/day (consumer), 1,500/day (Workspace). A quarterly board pack distribution script that sends to a 200-person list will fail on consumer accounts. On Workspace, a daily digest plus ad-hoc sends can approach the limit.
External URL fetch: UrlFetchApp has a 20,000 call/day limit and a 50MB response size cap. Relevant if you're pulling market data or bank feeds.
Properties storage: PropertiesService stores at most 500KB per script and 9KB per property. If you're serializing large arrays as checkpoint state (a common pattern), you'll exceed this without realizing it.
When to Stop Fighting the Quota
The continuation pattern solves the time problem but not the complexity problem. A multi-stage automation that chunks across multiple trigger invocations, saves state in PropertiesService, cleans up its own triggers, and handles partial failures gracefully is now a software project - not a spreadsheet automation.
For FP&A use cases where the logic is complex but the interaction is simple ("refresh my FCFF assumptions from this quarter's actuals and recalculate terminal value"), a server-side approach sidesteps the quota entirely. Tools like ModelMonkey run outside Apps Script - there's no 6-minute timer, no batch size management, and no continuation tokens. You describe what needs to happen across your 8-tab model, it executes it, and writes back the results.
That's not always the right call. If you have a working 3-minute script that runs on a trigger and never changes, don't break it. But if you're engineering around the quota, it's worth asking whether the engineering time is better spent elsewhere.