// Gmail · Apps Script

Count unread emails by label in Gmail.

Use GmailApp.getUserLabelByName() and label.getUnreadCount() to get an accurate unread count per Gmail label in Apps Script — no thread iteration required.

I want to read the unread count for a specific Gmail label in Apps Script without looping through every thread.

The script

copy · paste · trigger
countUnreadByLabel.gs
Apps Script
// Returns the unread message count for a named Gmail label.
// Run countUnreadByLabel() manually or call getUnreadForLabel() from other scripts.

function getUnreadForLabel(labelName) {
  var label = GmailApp.getUserLabelByName(labelName);
  if (!label) {
    throw new Error('Label not found: ' + labelName);
  }
  return label.getUnreadCount();
}

function countUnreadByLabel() {
  var labels = ['newsletter', 'support/open', 'leads'];
  var results = {};
  for (var i = 0; i < labels.length; i++) {
    results[labels[i]] = getUnreadForLabel(labels[i]);
  }
  Logger.log(results);
  return results;
}

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

Walkthrough

One call, one number

GmailLabel.getUnreadCount() queries Gmail's index directly. It returns the integer unread message count for that label in a single API call, the same count you see in the Gmail sidebar. There is no pagination, no thread fetching, nothing to aggregate.

Getting there takes two steps: GmailApp.getUserLabelByName('your-label') returns a GmailLabel object (or null if the label does not exist), and then .getUnreadCount() on that object gives you the number. The null check matters — a missing label silently returns null, and calling .getUnreadCount() on null throws a TypeError that looks completely unrelated to a label name typo.

For sublabels, pass the full path with a forward slash: getUserLabelByName('support/open'). That is how Gmail stores nested labels internally, and the string must match exactly, including case.

Why iterating threads gets the count wrong

The obvious alternative — call label.getThreads(), then sum the unread messages inside each thread — produces a different number than the sidebar shows, and not in a predictable way.

Gmail's unread count on a label is per-message, not per-thread. A thread with three messages, two of which are unread, contributes 2 to the label's unread count. If you fetch threads and check GmailThread.isUnread() (which returns true if any message in the thread is unread), you get a thread-level boolean, not a message count. If you fetch all messages and filter, you pay for multiple GmailApp.getMessages() calls, hit the 100-thread-per-page limit, and still end up with a number that matches the sidebar only by accident.

I have watched this bite people who built dashboards from thread iteration and then noticed their counts were always lower than Gmail's. The fix was a one-line replacement with getUnreadCount(). The thread-iteration code was not wrong in intent — it was just measuring something different.

Staying inside quota

Apps Script enforces a Gmail read quota of 20,000 API calls per day on consumer accounts and 50,000 on Workspace. Each getUserLabelByName() call and each getUnreadCount() call each count as one unit against that quota. Fetching three labels costs six calls total — trivial.

If you are polling many labels on a timer (say, every 5 minutes via a time-driven trigger), cache the GmailLabel objects in script properties or PropertiesService rather than calling getUserLabelByName() on every run. The label object itself is stable; its getUnreadCount() result is what changes. Fetching the label object once per day and the count on every tick cuts your quota usage roughly in half for a multi-label dashboard.

The getUnreadCount() method does not cache — it hits the Gmail API on every call, so the number is always current.

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
getUserLabelByName returns null even though the label exists — why?
The name is case-sensitive and must match exactly, including any parent path. A label named 'Support/Open' will not be found by getUserLabelByName('support/open'). Check the exact string in Gmail's label list (Settings > Labels) and copy it character-for-character, including the slash separator for nested labels.
Can I count unread emails in a Gmail system label like Inbox or Spam?
Not with getUserLabelByName() — that method only works on user-created labels. For Inbox, use GmailApp.getInboxUnreadCount(). There is no built-in equivalent for Spam or Trash unread counts via Apps Script; you would need the Gmail REST API with the labels.get endpoint and labelId 'SPAM' or 'TRASH' via UrlFetchApp.
getUnreadCount() returns 0 but I can see unread emails under that label in Gmail — what is happening?
Confirm the label is applied directly to those messages, not just to a parent label. Gmail does not roll up child-label unread counts to the parent. A message labeled 'support/open' does not increment the unread count of a separate 'support' label — only 'support/open' gets the credit. Run Logger.log(label.getName()) to confirm you have the right object.
How do I get unread counts for all my labels at once without knowing their names?
Call GmailApp.getUserLabels() to get an array of every user-created GmailLabel, then iterate and call getUnreadCount() on each. That is one call for the list plus one per label, so for 20 labels you use 21 quota units total — still well within daily limits.