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.sleepbetween 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
bodyandhtmlBodytogether. 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.sendEmail | GmailApp.sendEmail | |
|---|---|---|
| Speed | Slightly faster | Slightly slower |
| Creates a Gmail thread | No | Yes — a real, replyable message |
| Appears in Sent folder | No | Yes |
| Best for | Fire-and-forget notifications | Correspondence you may reply to |
| Extra features | Minimal | Labels, 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
sendEmailcall 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.sleepbetween 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.