How to Chunk Google Apps Script Code to Avoid Exceeded Maximum Execution Time

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 TypeMax 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:

  1. Progress tracking (where did we stop?)
  2. A hard limit per run
  3. A time-based trigger to resume
  4. 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.