Apps Script has a ceiling
How to make Apps Script carry on past the six-minute ceiling: a script that processes the rows of a spreadsheet in batches, stops before it hits the time ceiling, saves where it stopped, schedules its own return and cleans everything up when it finishes.
- Apps Script
- Google Sheets
- sample code
Published on
Read first: the limit of this guide
Apps Script quotas and limits change. Google’s own page says: "All quotas are subject to elimination, reduction, or change at any time, without notice." The numbers in this guide are the ones on the official page on November 10, 2026; check the page before you rely on them. The code in this guide was tested once in Google Apps Script, on November 10, 2026, on a sample spreadsheet with 200 rows and with the limit lowered to ten seconds, to force the stop. The five-minute limit in the code and large spreadsheets were not tested: the real test is yours. Run it first on a test spreadsheet, with sample data, never on the spreadsheet you actually use.
What each thing is
- Execution: each time the script runs from start to finish (or until it is interrupted). According to Google’s limits page, the script runtime is six minutes per execution, for both consumer and Workspace accounts.
- What happens when you go over the limit: the page says "If you exceed a quota or limitation, your script throws an exception and execution stops." The page does not give the exact message for the six-minute ceiling, so this guide does not quote one.
- Batch: a chunk of the spreadsheet (twenty rows in the example) processed at once, instead of one row at a time.
- Script properties (PropertiesService): where the script stores simple data in key-value pairs. The page says "Allows scripts to store simple data in key-value pairs". Here they hold the next row to process.
- Time-driven trigger: makes a function run after an interval or at a set time. You can create and delete them in code ("Create and delete triggers programmatically with the Script service."). The
after(milliseconds)method "Specifies the minimum duration (in milliseconds) after the current time that the trigger runs." - Other limits on the same page (consumer and Workspace accounts): email recipients per day, 100 and 1,500 (see the note on trial accounts below); total trigger runtime per day, 90 minutes and 6 hours; triggers per user per script, 20 for both.
Sample spreadsheet (made-up data, EXAMPLE)
Use a new spreadsheet, just for testing. Open Extensions, Apps Script (the script is bound to the spreadsheet). Paste the code below and run the criarDadosDeExemplo function once: it creates the Exemplo sheet with two hundred rows and the columns item, valor and resultado, all made up.
The code is the same as in the Portuguese guide, so its names are in Portuguese: criarDadosDeExemplo creates the sample data, iniciar starts from scratch, processarEmLotes processes in batches, apagarGatilhos deletes triggers, agendarContinuacao schedules the return, LIMITE_MS is the time limit, TAMANHO_LOTE is the batch size and proximaLinha is the next row.
The code to copy
Complete file. Paste it into the Apps Script editor in place of whatever is there.
const NOME_ABA = 'Exemplo';
const TAMANHO_LOTE = 20;
const LIMITE_MS = 5 * 60 * 1000;
const CHAVE = 'proximaLinha';
const FUNCAO = 'processarEmLotes';
function criarDadosDeExemplo() {
const planilha = SpreadsheetApp.getActiveSpreadsheet();
const aba = planilha.getSheetByName(NOME_ABA) || planilha.insertSheet(NOME_ABA);
aba.clear();
const linhas = [['item', 'valor', 'resultado']];
for (let i = 1; i <= 200; i++) {
linhas.push(['Item ' + i, i, '']);
}
aba.getRange(1, 1, linhas.length, 3).setValues(linhas);
}
function apagarGatilhos() {
ScriptApp.getProjectTriggers().forEach(function (gatilho) {
if (gatilho.getHandlerFunction() === FUNCAO) {
ScriptApp.deleteTrigger(gatilho);
}
});
}
function agendarContinuacao() {
ScriptApp.newTrigger(FUNCAO).timeBased().after(60 * 1000).create();
}
function iniciar() {
PropertiesService.getScriptProperties().deleteProperty(CHAVE);
apagarGatilhos();
processarEmLotes();
}
function processarEmLotes() {
const inicio = Date.now();
const propriedades = PropertiesService.getScriptProperties();
const aba = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(NOME_ABA);
const ultimaLinha = aba.getLastRow();
let linha = Number(propriedades.getProperty(CHAVE)) || 2;
apagarGatilhos();
while (linha <= ultimaLinha) {
if (Date.now() - inicio > LIMITE_MS) {
agendarContinuacao();
return;
}
const quantidade = Math.min(TAMANHO_LOTE, ultimaLinha - linha + 1);
const valores = aba.getRange(linha, 2, quantidade, 1).getValues();
const resultados = valores.map(function (celula) {
return [celula[0] * 2];
});
aba.getRange(linha, 3, quantidade, 1).setValues(resultados);
Utilities.sleep(2000);
linha += quantidade;
propriedades.setProperty(CHAVE, String(linha));
}
propriedades.deleteProperty(CHAVE);
}How to test
- Run
criarDadosDeExemplo. Authorize the script when Google asks (it is your own test spreadsheet). - To see the trigger working without waiting five minutes, replace the limit line with
const LIMITE_MS = 10 * 1000;(ten seconds). The two-second sleep per batch simulates heavy work. - Run the
iniciarfunction. It resets the marker, deletes old triggers for this function and processes until it goes past the limit. After about ten seconds, it stops and schedules its return. - Under Triggers (the clock icon in the sidebar), a time-driven trigger for
processarEmLotesshould appear. Under Project Settings, Script Properties, theproximaLinhakey should appear with the next row to process. - Wait about a minute, or more: the trigger runs on its own and continues from where it stopped. Repeat the check until the resultado column is filled in for all two hundred rows.
- At the end, there should be no trigger left for
processarEmLotesand noproximaLinhakey. - Set
LIMITE_MSback to5 * 60 * 1000(five minutes, a one-minute margin below the six-minute ceiling) and replace the example calculation (value times two) with your real work, still on a copy.
How the code works
processarEmLotesrecords the start time and reads the next row saved in the script properties (if there is none, it starts on the second row, right below the header).- It deletes old triggers for the function itself, so they do not pile up.
- It processes one batch at a time and writes the result to the spreadsheet. After each batch, it saves the next row. If Google interrupts it abruptly: since the write to the spreadsheet comes before the property is saved, the batch in progress may be repeated in the next execution.
- Before each batch, it checks whether the limit has passed. If it has, it schedules a time-driven trigger for one minute later and ends cleanly.
- When the rows run out, it deletes the property. Since the old trigger is deleted at the start of each execution, none is left at the end.
Rule of thumb
Work that may take more than six minutes never runs in a single execution: split it into batches, check the clock at every batch, save where you stopped and schedule the return. Stop with a margin (one minute in the example), not at the last second.
Before you take it to your project
- Test on a copy of the spreadsheet, not on the original file.
- The example script is bound to the spreadsheet. In a standalone script, replace
SpreadsheetApp.getActiveSpreadsheet()withSpreadsheetApp.openById('ID_DA_PLANILHA'), using the ID of your test spreadsheet. - Installable triggers run under the account of whoever created them: "Installable triggers always run under the account of the person who created them."
- Total trigger runtime counts per day (90 minutes for consumer accounts and 6 hours for Workspace, according to the page on November 10, 2026). A very large job can hit that limit too.
- If your batch sends email, the daily recipient quota applies (100 for consumer accounts and 1,500 for Workspace, according to the page). The page mentions the
MailApp.getRemainingDailyQuota()method to see what is left. The page says: "Additional limits apply for trial accounts." For trial accounts converted to a subscription, it says the limits increase once the domain has cumulatively paid at least USD 100 and at least 60 days have passed since reaching that amount. Check your case on the page. The table also has the row "Email recipients per day within the domain": 100 in the consumer column and 2,000 in the Workspace column. - Reading and writing a whole batch at once, as the code does, is usually faster than going cell by cell. This is general spreadsheet care, not taken from Google’s page.
- If a batch fails midway, the marker still points to its start: check that reprocessing the same rows does no harm (for example, an email sent twice). After an error, running
processarEmLotesagain continues from the saved row (useiniciaronly to start over from scratch). After an error no trigger stays scheduled, so resuming is manual. - The code does not use LockService. Do not run the function by hand while a trigger execution is in progress, so that two executions do not process the same rows.
after()sets the minimum time until the trigger runs, not an exact time.- The five-minute limit is a recommendation of this guide (a one-minute margin below the ceiling), not a number from the page.
Sources
Google for Developers pages opened on November 10, 2026: Quotas for Google Services (English), Quotas for Google Services (Portuguese), Installable Triggers, ClockTriggerBuilder, PropertiesService and Properties.
Code: tested in Google Apps Script on November 10, 2026, on a sample spreadsheet with 200 rows and with the limit lowered to ten seconds: the execution stopped at the limit, saved the next row in the script properties, the time-driven trigger continued on its own and, at the end, no trigger or property was left.
Guide wrap-up
You now know how to build a script that processes the spreadsheet in batches, stops before the six-minute ceiling and carries on by itself from where it stopped.
Follow @drasdiegotechBookmark this guide in your browser and save the reel on Instagram so you can come back when you need it.
This site counts visits with Vercel Web Analytics, anonymously and in aggregate, without third-party cookies. Privacy notice