// Gmail · Apps Script

Mark emails as read by search in Gmail.

Use Apps Script to mark every email matching a Gmail search query as read in one batched call, avoiding the per-message quota trap.

I want to run a Gmail search and mark all matching threads as read without burning through my script quota by looping over individual messages.

The script

copy · paste · trigger
markReadBySearch.gs
Apps Script
// Mark every thread matching a Gmail search query as read.
// Run from the Apps Script editor or attach to a time-based trigger.

function markReadBySearch() {
  var query = 'label:notifications is:unread older_than:7d';
  var batchSize = 100;
  var start = 0;
  var threads;

  while (true) {
    threads = GmailApp.search(query, start, batchSize);
    if (threads.length > 0) {
      GmailApp.markThreadsRead(threads);
    }
    start += threads.length;
    if (threads.length < batchSize) return;
  }
}

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

Walkthrough

Why batching matters here

GmailApp.search returns an array of GmailThread objects. GmailApp.markThreadsRead accepts that entire array and issues one underlying API call for all of them. The alternative — iterating over threads, calling getMessages on each, then calling markRead on each message — fires one API call per message. Gmail quota for Apps Script is 20,000 calls per day on a personal account. A notification label with 1,500 unread messages eats 7.5% of your daily budget in a single run. The batch call costs a fraction of that regardless of thread count.

The 100-thread limit per search call is a hard cap in the GmailApp API, which is why the script pages: start increments by however many threads actually came back, and the loop exits when the result is shorter than the requested batch (meaning there are no more pages).

Adapting the search query

The query string is plain Gmail search syntax — the same thing you type in the search box. 'is:unread' scopes to unread threads. 'label:notifications' narrows to a specific label. 'older_than:7d' limits to threads older than seven days. Any combination that works in Gmail's search bar works here.

If you want to mark everything from a sender: change query to 'from:noreply@example.com is:unread'. To target a date range: 'before:2024/01/01 is:unread'. The script does not care what the query is — it just pages through the results and batches the markRead call.

One thing I keep in a utils file: a thin wrapper that accepts the query as a parameter so one trigger can call markReadBySearch with different queries on a schedule rather than duplicating the function. The do-while structure lifts out cleanly into a helper that takes query and returns the count of threads processed.

Attaching a time-based trigger

To run this on a schedule without touching the editor, open the Apps Script project, go to Triggers (the alarm-clock icon), and create a new trigger: function markReadBySearch, event source Time-driven, type Day timer or Hour timer, at whatever interval makes sense for your volume.

The script will run under your Google account's permissions after you authorize it the first time. The required OAuth scope is https://mail.google.com/ — Apps Script will prompt for it on the first manual run. If you see a 'Gmail operation not allowed' error, the script has not been authorized yet; run it once from the editor to trigger the consent screen.

Execution time limit for Apps Script is 6 minutes on the free tier. For inboxes with tens of thousands of matching threads, a single run may not drain the full result set. In that case, run the trigger more frequently rather than trying to process everything in one shot — each run pages from start=0 and will eventually converge as markThreadsRead shrinks the 'is:unread' pool.

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
Why does GmailApp.search stop at 500 results even with paging?
GmailApp.search has an undocumented cap of 500 threads total per query regardless of paging. If you have more than 500 matching threads, run the script multiple times — each run marks the first 500 as read, which removes them from future 'is:unread' results, so subsequent runs process the next batch.
What is the difference between markThreadsRead and markMessagesRead?
markThreadsRead operates on GmailThread objects and marks all messages in each thread as read in one call. markMessagesRead operates on GmailMessage objects and requires you to have already fetched the messages via getMessages. Use markThreadsRead unless you need to selectively mark only some messages within a thread.
Can I run this on a shared inbox or a Google Workspace alias?
GmailApp always operates on the account that authorized the script. There is no built-in way to target a different address. For a shared inbox, the script must be deployed under that inbox's account, or you must use the Gmail REST API with domain-wide delegation, which requires Workspace admin access.
The script runs but some threads are still unread. Why?
The most common cause is that the threads matched the query at search time but were already being modified by another process (a filter, a mobile client syncing) before markThreadsRead fired. Less commonly, the 500-thread cap cut off the result set. Re-running the script is safe — markThreadsRead on an already-read thread is a no-op.