Skip to content
// Gmail · Apps Script

Delete emails older than 30 days in Gmail.

A Google Apps Script that pages through Gmail search results in batches and moves old threads to trash — working around the 500-thread search cap and 100-thread trash limit.

I need a script that automatically clears out emails older than 30 days so my inbox stops accumulating years of noise.

The script

copy · paste · trigger
deleteOldEmails.gs
Apps Script
// Deletes all Gmail threads older than 30 days, in pages of 100.
// Run manually or attach to a time-based trigger.
function deleteOldEmails() {
  var query = 'older_than:30d';
  var batchSize = 100;
  var start = 0;
  var threads;

  do {
    threads = GmailApp.search(query, start, batchSize);
    if (threads.length === 0) break;
    GmailApp.moveThreadsToTrash(threads);
    start += threads.length;
  } while (threads.length === batchSize);
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why a plain search-and-trash loop breaks on large inboxes

GmailApp.search(query) with no offset arguments caps at 500 threads. Call it on a backlog of 4,000 old emails and you silently process only the first 500, then stop. The function returns no error and gives no indication it left anything behind.

The other foot-gun: moveThreadsToTrash accepts an array, but passing more than 100 threads in a single call throws a 'Service invoked too many times' error mid-batch. Splitting the work into pages of 100 sidesteps both limits at once.

The three-argument form GmailApp.search(query, start, max) is the fix. It behaves like SQL LIMIT/OFFSET: start is the index of the first result to return, max is how many to return. The do-while loop increments start by however many threads actually came back, so a partially-filled last page (fewer than batchSize results) naturally ends the loop.

Attaching a time-based trigger so it runs without you

Open the script in Apps Script (script.google.com), go to Triggers (the clock icon), and add a new trigger: function deleteOldEmails, time-driven, day timer, once a day. The first run will ask for OAuth consent — Gmail modify scope. After that it runs headlessly on Google's servers.

One thing to know about Apps Script execution limits: a single run is capped at 6 minutes. If your backlog is enormous (tens of thousands of threads), a single execution may not finish. The loop will still make progress each day, clearing 100 threads per iteration per run until the backlog is gone. Once steady-state, daily runs handle whatever arrived 30+ days ago with plenty of headroom.

I keep the trigger on a nightly schedule around 2 AM so it doesn't compete with anything interactive. Daily is frequent enough; running it hourly on an already-clean inbox wastes quota for no reason.

Narrowing the query before you commit

older_than:30d matches everything: newsletters, receipts, threads you'd rather keep. Before the first real run, paste the query directly into Gmail's search bar to see exactly what will be trashed. Gmail's search operators are the same ones Apps Script accepts.

Common refinements: 'older_than:30d label:newsletters' targets only a specific label, 'older_than:30d -is:starred' excludes anything you've starred, and 'older_than:30d in:promotions' limits deletion to the Promotions tab. Combine them with spaces (AND is implicit). Swap the query string in the script once you're satisfied with what the search returns.

Trash in Gmail isn't permanent deletion — messages sit there for 30 days before Google removes them. If you run the script and immediately regret it, open Trash and restore. For permanent immediate deletion you'd call thread.moveToTrash() followed by Gmail.Users.Threads.remove(), but that requires enabling the Advanced Gmail Service and is irreversible, so the soft-delete approach is the right default.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
The script runs but emails are still in my inbox — what's wrong?
moveThreadsToTrash moves threads to Trash, not to permanent deletion. Check Gmail's Trash folder; the emails are there. If you want them out of Trash immediately, you need Gmail.Users.Threads.remove() via the Advanced Gmail Service, which permanently deletes and cannot be undone.
How do I change the cutoff from 30 days to 60 days?
Change the query string to 'older_than:60d'. The older_than operator accepts d (days), m (months), and y (years), so 'older_than:2y' works too.
Can I delete emails from a specific sender only?
Yes. Combine operators in the query string: 'older_than:30d from:[email protected]'. Standard Gmail search syntax applies — from:, label:, has:attachment, in:promotions, etc.
Will this trigger hit Apps Script's daily quota?
GmailApp.search and moveThreadsToTrash both count against Gmail service calls, which have a 20,000 calls/day quota for consumer accounts. A single daily run of this script uses at most a few hundred calls even on a large backlog, so quota is not a practical concern unless you're running many other Gmail scripts in the same account.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.