https://github.com/lsvekis/Apps-Script-Code-Snippets/tree/main/google-apps-script-chunking-pattern
If you’ve ever run a Google Apps Script and hit this dreaded error:
Exceeded maximum execution time
—you’re not alone.
This happens a lot when working with Google Docs, Sheets, Drive, or Gmail, especially when looping through large documents, spreadsheets, or folders.
The good news?
You don’t need to rewrite everything or give up on your automation.
The solution is chunking.
In this post, you’ll learn:
- Why Apps Script times out
- What “chunking” really means
- How to chunk long-running scripts safely
- A reusable chunking pattern you can apply to any script
- A complete real-world example with triggers and progress tracking
Why Google Apps Script Times Out
Google Apps Script has hard execution limits:
| Account Type | Max Runtime |
|---|---|
| Consumer (free) | ~6 minutes |
| Google Workspace | ~30 minutes (varies by service) |
Operations that are especially slow:
DocumentApp.insertParagraph()removeChild()getText()in large Docs- Formatting operations
- Nested loops
- Processing thousands of rows or elements
If your script tries to do everything in one run, it will eventually hit the wall.
What “Chunking” Means (In Simple Terms)
Chunking means:
“Do a small amount of work, stop early, save progress, then continue later.”
Instead of processing 100% of the task at once, you process:
- 20–50 items per run
- for 30–60 seconds
- then automatically resume
Think of it like pagination for code.
The Core Chunking Strategy
A robust chunked script needs four pieces:
- Progress tracking (where did we stop?)
- A hard limit per run
- A time-based trigger to resume
- Cleanup when finished
Step 1: Store Progress Between Runs
Use PropertiesService to remember where you left off.
const props = PropertiesService.getDocumentProperties();
// Save progress
props.setProperty("CURRENT_INDEX", String(i));
// Resume later
let i = Number(props.getProperty("CURRENT_INDEX"));
This is the backbone of chunking.
Step 2: Limit Work Per Execution
Never rely on “it should finish in time”.
Use both:
- A maximum item count
- A maximum time budget
const MAX_ITEMS_PER_RUN = 40;
const MAX_MS = 60 * 1000; // 1 minute
This guarantees safety.
Step 3: Chunked Processing Loop
Here’s a safe, reusable chunk loop:
function processChunk() {
const start = Date.now();
let processed = 0;
while (
hasMoreWork() &&
processed < MAX_ITEMS_PER_RUN &&
(Date.now() - start) < MAX_MS
) {
processOneItem();
processed++;
}
}
You stop before Google stops you.
Step 4: Resume Automatically with Triggers
If there’s more work left, schedule the next chunk.
function scheduleNextRun() {
ScriptApp.newTrigger("processChunk")
.timeBased()
.after(30 * 1000)
.create();
}
This allows your script to run hands-free until completion.
Full Real-World Example
Chunking a Google Docs Cleanup Script
This example converts bullet list items into dash (-) paragraphs without timing out, even on huge documents.
Start Function (Run Once)
function convertBulletsStart() {
const props = PropertiesService.getDocumentProperties();
props.deleteProperty("INDEX");
ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t));
convertBulletsStep();
}
Chunk Processor
function convertBulletsStep() {
const lock = LockService.getDocumentLock();
if (!lock.tryLock(15000)) return;
try {
const doc = DocumentApp.getActiveDocument();
const body = doc.getBody();
const props = PropertiesService.getDocumentProperties();
let i = Number(props.getProperty("INDEX"));
if (!Number.isFinite(i)) i = body.getNumChildren() - 1;
const MAX_ITEMS = 30;
const MAX_MS = 45 * 1000;
const start = Date.now();
let processed = 0;
while (
i >= 0 &&
processed < MAX_ITEMS &&
(Date.now() - start) < MAX_MS
) {
const el = body.getChild(i);
if (el.getType() === DocumentApp.ElementType.LIST_ITEM) {
const li = el.asListItem();
const text = li.getText().trim();
if (text === "") {
body.removeChild(li);
} else {
const p = body.insertParagraph(i, "- " + text);
p.setAttributes(li.getAttributes());
body.removeChild(li);
}
processed++;
}
i--;
}
props.setProperty("INDEX", String(i));
if (i >= 0) {
ScriptApp.newTrigger("convertBulletsStep")
.timeBased()
.after(30 * 1000)
.create();
} else {
props.deleteProperty("INDEX");
DocumentApp.getUi().alert("Done converting bullets!");
}
} finally {
lock.releaseLock();
}
}
Why This Pattern Works
✅ Avoids execution timeouts
✅ Survives large documents
✅ Safe against overlapping runs
✅ Resumable after failures
✅ Production-ready
This same pattern works for:
- Cleaning Google Docs
- Processing large Sheets
- Renaming Drive files
- Sending batch emails
- Data migrations
- AI document processing
Pro Tips for Faster Chunking
- Avoid UI alerts inside loops
- Minimize formatting calls
- Batch work where possible
- Prefer modifying existing elements instead of insert/remove
- Lower chunk size for heavily formatted Docs
When Chunking Isn’t Enough
If your script is still slow:
- Switch to the Google Docs API batchUpdate
- Move heavy logic to Sheets or Drive metadata
- Pre-filter items before processing
- Use hyphen glyphs instead of replacing bullets with text
Final Thought
If your Apps Script ever feels “fragile”, chunking turns it into a reliable, scalable system.
Once you start chunking, you’ll wonder how you ever wrote scripts without it.