Gmail spring clean

20 May 2026

One Gmail account, opened around 2011. Fifteen years later it held roughly 105,000 messages. The overwhelming majority was marketing — Amazon Kindle Daily Deals arriving every single day since 2014, cashback newsletters, deal aggregators, newsletters from services that no longer exist. None of it ever read. All of it quietly counted against the storage quota.

Spring clean time. Here's how it went.

Sample before you delete

The temptation is to start binning immediately. Bad idea — somewhere in fifteen years of mail are things worth keeping: tax-relevant broker statements, booking confirmations, receipts. So the first job was reconnaissance.

I searched the archive by era — pulled samples from 2016, 2018, 2021 — and looked at who the mail was from. The pattern was stark: a few dozen senders accounted for an enormous share of the volume. amazon-offers, stackcommerce, Travelzoo, Photobox, a vape shop, a broker I left years ago. That became a target list: senders that had earned zero benefit of the doubt.

Gmail's own tools give up

Gmail can bulk-delete. You search, tick "select all conversations that match", hit the bin. But the operation caps at about 10,000 conversations at a time, and for ~80,000 messages that's a lot of repetitive clicking — assuming the select-all link behaves, which it didn't always. This needed to be scriptable.

The right tool turned out to be Google Apps Script. It runs under your own Google account, with full Gmail access, for free. The first version was a dozen lines: search for matching threads, move them to trash, repeat.

The optimisation journey

That first version did not survive contact with reality. The script went through five rewrites in an afternoon, each one fixing a constraint discovered by actually running it:

  1. v1 — naïve loop. Apps Script kills any single execution after 6 minutes. v1 didn't finish even one query before being terminated.
  2. v2 — auto-resume. The fix: when the run is about to hit the time limit, save your place, schedule a one-shot trigger to fire one minute later, and exit cleanly. On wake-up it reads the saved state and carries on. It now ran unattended across as many slices as it needed.
  3. v3 — a proper report. Added per-query statistics — counts, date ranges, the top senders — accumulated across all slices and emailed to myself when the whole sweep finished. Now there was a record of exactly what vanished.
  4. v4 — lazy sampling. Gathering those sender stats meant one extra Gmail fetch per sampled thread, and that had become the bottleneck. Restricted sampling to the first few batches of each query. Roughly twice as fast.
  5. v5 — the batch endpoint. The big one. The friendly wrapper, moveThreadsToTrash, is capped at 100 threads per call. But Gmail's underlying API has batchModify — up to 1,000 messages in a single call. Switching to the advanced Gmail service made the whole thing roughly an order of magnitude faster.

Then a second round (call it v6): with the first pass done, I re-sampled what had survived, tightened the date filters, and added about forty more senders the first run had surfaced.

The result

Broker statements, Airbnb confirmations, anything that was actually a record — deliberately excluded by the queries and left untouched. And a safety net throughout: anything ever starred or marked important was skipped entirely.

Staying clean

A one-off purge is only half the job — without a change, the inbox just refills. So the cleanup was paired with a set of Gmail filters that route incoming marketing straight to Trash on arrival. Gmail empties Trash automatically after 30 days, which makes it a free, zero-maintenance "timer until die" for promotional mail. The inbox now holds steady instead of growing.

The takeaway

Two things worth keeping. First: sample before you delete — fifteen years of mail is not uniformly junk, and ten minutes of reconnaissance is what lets you be aggressive safely. Second, the engineering one: when a platform's convenience wrapper is slow, look for the batch endpoint underneath it. The difference between moveThreadsToTrash and batchModify was the difference between "leave it running overnight" and "watch it finish".


richmorgan.co.uk