Concept
The limits you'll actually hit
Google publishes the full list on its Quotas for Google Services page, and the numbers change from time to time — always check there for current values. These are the ones that matter most:
| Limit | Personal Gmail account | Google Workspace account |
|---|---|---|
| Run time per execution | ~6 minutes | ~6 minutes |
| Custom function run time | ~30 seconds | ~30 seconds |
| Email recipients per day | ~100 | ~1,500 |
| Total trigger run time per day | ~90 minutes | ~6 hours |
| Triggers per user per script | 20 | 20 |
The email and 6-minute limits are the ones most beginners run into.
Check email quota before sending
function sendIfQuota(recipients) {
const left = MailApp.getRemainingDailyQuota();
if (left < recipients.length) {
console.warn(`Only ${left} emails left today; need ${recipients.length}.`);
return false;
}
// ... send ...
return true;
}
Beating the 6-minute limit: work in chunks
Save your position, stop before time runs out, and continue on the next run.
const MAX_MS = 5 * 60 * 1000; // stop after 5 minutes, leaving a safety margin
function processInChunks() {
const start = Date.now();
const props = PropertiesService.getScriptProperties();
let nextRow = Number(props.getProperty('NEXT_ROW') || 2);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
const lastRow = sheet.getLastRow();
while (nextRow <= lastRow) {
if (Date.now() - start > MAX_MS) break;
const batchSize = Math.min(50, lastRow - nextRow + 1);
const rows = sheet.getRange(nextRow, 1, batchSize, 3).getValues();
rows.forEach(r => {
// ... slow work per row, e.g. an API call ...
});
nextRow += batchSize;
props.setProperty('NEXT_ROW', String(nextRow)); // save progress after each batch
}
if (nextRow > lastRow) {
props.setProperty('NEXT_ROW', String(nextRow)); // retain completion cursor
console.log('All rows done');
} else {
console.log(`Paused at row ${nextRow}; will continue on next run`);
}
}
Pair this with a time-driven trigger every 10–15 minutes. When the job finishes, the cursor stays after the last processed row; later runs only process newly appended rows. Delete NEXT_ROW manually only when you intend to restart the whole job.
Cache slow lookups
If many runs fetch the same data (e.g. today's gold rate from an API), cache it:
function getRate() {
const cache = CacheService.getScriptCache();
const cached = cache.get('GOLD_RATE');
if (cached) return Number(cached);
const rate = fetchRateFromApi(); // slow call (your own function)
cache.put('GOLD_RATE', String(rate), 3600); // keep for 1 hour (max 6 hours)
return rate;
}
Prevent two runs clashing
If a trigger and a user can run the same function at the same time (e.g. both appending invoice numbers), use a lock:
function nextInvoiceNumber() {
const lock = LockService.getScriptLock();
lock.waitLock(10000); // wait up to 10 s
try {
const props = PropertiesService.getScriptProperties();
const n = Number(props.getProperty('LAST_INVOICE') || 0) + 1;
props.setProperty('LAST_INVOICE', String(n));
return n;
} finally {
lock.releaseLock();
}
}
Other speed tips
- Batch reads/writes (Module 2) solve most "too slow" problems.
- Avoid
SpreadsheetApp.flush()inside loops. - Don't open the same file again and again — open once and pass it around.
Key points
- Plan for ~6 minutes per run and daily email limits; check Google's quota page for current numbers.
- Big jobs: save progress in Script Properties and continue on the next trigger.
CacheServicefor repeated lookups,LockServicefor shared counters.