appscript.dev
Guide Intermediate Gmail

Send reliable email at scale

Avoid Gmail spam folders and quota limits when Northwind sends thousands of emails.

Published Sep 13, 2025

Sending one email from Apps Script is trivial. Sending thousands reliably is a different problem, because at volume two things start to fight you: Google’s daily quotas, and the spam filters on the receiving end. A script that ignores both will either stop dead halfway through a run or quietly land every message in a junk folder.

Reliable bulk email is less about the code and more about discipline — pacing the sends, authenticating the domain, and giving recipients a way out. This guide covers the practical rules that keep messages delivered and the run finished.

Practical rules

Most deliverability problems are avoided before a single message goes out. These are the habits that keep a sending domain in good standing.

  • Warm up the domain. A brand-new domain that suddenly sends 5,000 emails looks exactly like a compromised account. Ramp volume up over days or weeks so the reputation builds gradually.
  • Pace sends. One to two messages per second is safe. Bursting faster invites spam-filter scrutiny and can trip rate limits — use Utilities.sleep between sends to space them out.
  • Configure SPF, DKIM, and DMARC. All three DNS records must be set on the sending domain. Without them, many providers downgrade or reject mail outright, and there is nothing in the script that can compensate.
  • Include a per-recipient unsubscribe link. This is a legal requirement in most jurisdictions for bulk or marketing mail, and a missing or broken one is a fast route to being blocklisted.
  • Set both a plain-text and an HTML body. Pass body and htmlBody together. Some clients and filters distrust HTML-only messages, and the plain-text part is the fallback when HTML cannot render.

The single biggest lever is pacing. A steady trickle of well-formed messages looks like normal traffic; a sudden flood looks like an attack.

Track failures

GmailApp.sendEmail returns void — it tells you the message was handed off, not that it was delivered. A bad address, a full mailbox, or a hard rejection all produce a bounce that arrives later, in a separate reply, with no exception thrown at send time.

Because of that, you cannot detect delivery problems at the call site. The only reliable signal is the bounce messages that come back to the sending inbox, and those have to be scanned after the fact. To turn bounces into a clean list, see Detect bounced emails, which walks through parsing those replies and removing dead addresses so the next run does not waste quota on them.

Quota query

Before a bulk run, check that there is enough daily quota left to finish it. Starting a 900-message job with 600 sends remaining just means the last 300 fail.

// Returns true only if there is headroom to send n messages.
function canSend(n) {
  // getRemainingDailyQuota() is a live figure that resets every 24 hours.
  // The +50 buffer leaves room for retries and any other scripts that send.
  return MailApp.getRemainingDailyQuota() >= n + 50;
}

Call canSend once at the start of the run and bail out early if it returns false, rather than discovering the shortfall partway through. The buffer matters because the quota is shared across every script the account runs — another trigger may consume some of it while your job is in progress.

The quota itself depends on the account type: 100 recipients per day on a free Gmail account, 1,500 on most Workspace plans. A “recipient” is counted per address, so one email to 20 people costs 20.

When to use MailApp vs GmailApp

Apps Script offers two mail APIs. They look similar but behave differently, and picking the wrong one creates clutter or missing features.

MailApp.sendEmailGmailApp.sendEmail
SpeedSlightly fasterSlightly slower
Creates a Gmail threadNoYes — a real, replyable message
Appears in Sent folderNoYes
Best forFire-and-forget notificationsCorrespondence you may reply to
Extra featuresMinimalLabels, drafts, threads, attachments

Use MailApp for transactional, one-way mail — alerts, reports, receipts — where you never need to see or reply to the message. Use GmailApp when the email is part of a conversation a human will continue, because it leaves a real thread in the sending account.

Common mistakes

  • Treating a successful sendEmail call as proof of delivery. It only confirms hand-off; bounces arrive later and must be tracked separately.
  • Sending as fast as the loop allows. Without Utilities.sleep between messages, a large run both looks like spam and risks tripping rate limits.
  • Skipping SPF, DKIM, and DMARC. No amount of careful code fixes mail that the receiving server cannot authenticate.
  • Forgetting that recipients are counted individually. A single email to a 100-person list consumes 100 of the daily quota, not one.
  • Omitting the unsubscribe link from bulk mail. It is both a legal exposure and a fast path to a damaged sender reputation.
  • Starting a run without checking remaining quota, so the job dies partway and leaves recipients in an inconsistent state.