A nightly job that emails 300 people works fine until the day it doesn't, and then it dies halfway through with “Service invoked too many times for one day: email”. sendEmailWithBudget(message, opts) keeps its own daily send counter in Script Properties, keyed by today's date in the script's timezone. When the next send would go over your budget, it refuses and returns { sent: false, reason } instead of throwing mid-loop, so your job can stop cleanly and pick up tomorrow.
When to use
- Nightly or weekly email blasts from a sheet
- Reminder jobs that run on a trigger and might grow past the daily cap
- Any loop that calls
MailApp.sendEmailmore than a handful of times
How to use this snippet
/**
* Send one email, but never past a daily budget.
* Counter lives in Script Properties, keyed by date in the script timezone.
*
* @param {Object} message same object MailApp.sendEmail accepts ({ to, subject, body, htmlBody, ... })
* @param {Object=} opts { dailyBudget: number (default 90), delayMs: number (default 0) }
* @return {{sent: boolean, count?: number, reason?: string}}
*/
function sendEmailWithBudget(message, opts) {
opts = opts || {};
var budget = opts.dailyBudget || 90; // stay under the 100/day consumer cap
var props = PropertiesService.getScriptProperties();
var today = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd');
var key = 'MAIL_SENT_' + today;
var sent = Number(props.getProperty(key) || 0);
if (sent + 1 > budget) {
return { sent: false, reason: 'Daily budget reached (' + sent + '/' + budget + ')' };
}
// Belt and braces: Google's own count includes mail sent by your other scripts
if (MailApp.getRemainingDailyQuota() < 1) {
return { sent: false, reason: 'Gmail quota exhausted (getRemainingDailyQuota = 0)' };
}
if (opts.delayMs) Utilities.sleep(opts.delayMs);
MailApp.sendEmail(message);
props.setProperty(key, String(sent + 1));
return { sent: true, count: sent + 1 };
}
Example
function sendReminders() {
var sh = SpreadsheetApp.getActive().getSheetByName('Reminders');
var rows = sh.getDataRange().getValues(); // Email | Name | SentAt
for (var i = 1; i < rows.length; i++) {
if (rows[i][2]) continue; // already sent
var res = sendEmailWithBudget({
to: rows[i][0],
subject: 'Quick reminder',
body: 'Hi ' + rows[i][1] + ', your report is due Friday.'
}, { dailyBudget: 90, delayMs: 250 });
if (!res.sent) { console.log('Stopping: ' + res.reason); break; }
sh.getRange(i + 1, 3).setValue(new Date()); // checkpoint
}
}
Tips:
- Set
dailyBudgeta little under your real quota (100/day on consumer accounts, 1,500 on Workspace) so other scripts and manual tests still have room. MailApp.getRemainingDailyQuota()is Google's own count. Use it to sanity-check the budget before a big run.- Pair it with a SentAt column so a stopped run resumes where it left off instead of re-sending.
- Old
MAIL_SENT_keys are tiny, but you can clear anything older than a week in a monthly cleanup.
Tip: NitroGAS Co-Pilot can swap the MailApp.sendEmail calls in an existing loop for sendEmailWithBudget and add the SentAt checkpoint. Paste the function.
Happy Coding!
