Skip to content
// Apps Script

Fix "Service invoked too many times for one day" in Apps Script.

Why Apps Script quota errors hit before you expect them, how the 24-hour rolling window works, and how to batch API calls and cache results so you stop burning quota on repeat work.

I'm hitting "Service invoked too many times for one day" in my Apps Script and need to understand why the quota resets aren't happening when I expect them and how to restructure my code to stay under the limit.

The script

copy · paste · trigger
batchEmailSender.gs
Apps Script
// Batch outbound emails with a cache guard to avoid quota exhaustion
// Works for consumer (@gmail.com) and Workspace accounts
function sendBatchEmails(recipients) {
  var cache = CacheService.getScriptCache();
  var sentKey = 'sentCount_' + new Date().toDateString();
  var sent = parseInt(cache.get(sentKey) || '0', 10);

  // Consumer quota: 100/day; Workspace: 1500/day
  var DAILY_LIMIT = Session.getEffectiveUser().getEmail().endsWith('@gmail.com')
    ? 90
    : 1400;

  var batch = recipients.slice(0, Math.max(0, DAILY_LIMIT - sent));

  for (var i = 0; i < batch.length; i++) {
    GmailApp.sendEmail(batch[i].address, batch[i].subject, batch[i].body);
    sent++;
  }

  cache.put(sentKey, String(sent), 86400);
  return sent;
}

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

Walkthrough

The quota model is not what you think it is

Google publishes per-service daily limits in the Apps Script quotas table, and the numbers differ sharply by account type. Consumer accounts (anything ending in @gmail.com) get 100 outbound emails per day and 20,000 UrlFetch calls. Workspace accounts get 1,500 emails and 100,000 UrlFetch calls. The first time I hit this error, I spent an hour re-reading my code looking for a loop bug. There was no bug. I was just on a consumer account and had sent 101 emails to a test list.

The part the docs bury is that the reset is not at midnight in your timezone. Google uses a rolling 24-hour window anchored to when the first call in the current cycle was made, or possibly to UTC midnight — the exact anchor is undocumented and has changed across years. Treat it as 'somewhere in the next 24 hours' and plan accordingly. Waiting until 12:01 AM and retrying is a common trap; if your window anchors to 10 PM, you wait two hours for nothing.

This means the only reliable fix is spending less quota, not timing your way around the limit. Batch, cache, and skip repeat work.

Where quota leaks happen in practice

The most common source is a time-driven trigger that runs every hour and calls UrlFetch or GmailApp inside a loop over a dataset that grows over time. At 10 rows it is fine. At 300 rows and hourly triggers, you exceed the daily ceiling before noon.

The second common source is fetching data you already fetched. If your script calls an external API to look up the same record on every run, every run burns quota for data that did not change. CacheService is the fix here. Store the JSON response under a stable key with a TTL (time-to-live, how long the cache entry lives before expiring) that matches how often the source data actually changes. For reference data that changes weekly, a 6-hour TTL cuts UrlFetch calls by 5x on an hourly trigger.

Triggers themselves count. Each time-based trigger fires, Apps Script counts it against the trigger quota (20 triggers per script for consumer, not per user). More relevant: if you create triggers programmatically inside a loop by accident — a common mistake when a setup function runs more than once — you pile up duplicate trigger registrations, each burning quota independently. Check ScriptApp.getProjectTriggers() before creating a new one.

Structuring the fix: batch first, cache second, defer the rest

The snippet above shows the core pattern. Before sending anything, read a running count from CacheService. Compute how many sends remain under your self-imposed ceiling (I set mine 10 below the published limit as a buffer). Slice the work to that count, do it, write the updated count back to cache with an 86,400-second TTL (24 hours).

For UrlFetch calls, the pattern is the same but the cache key should be derived from the request parameters, not the date. Something like 'fetch_' + Utilities.computeDigest applied to the URL string gives you a stable key. Retrieve it first; only call UrlFetch if the cache misses.

Deferring work is the last resort but sometimes the right answer. If you have 2,000 emails to send and you are on a consumer account, no amount of batching sends them in one day. Split the job across days using PropertiesService to store a cursor (the index into your list where the last run stopped), and let the daily trigger advance the cursor by 90 each morning. The job takes 22 days. That is usually fine.

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
Does the quota reset at midnight Google time?
No. The window is rolling, not calendar-day-aligned. Google anchors it to UTC midnight in some accounts and to the first API call of the cycle in others — the behavior has varied and is not formally documented. Do not depend on a midnight reset; engineer around spending less quota per run.
My Workspace account still hits the limit. Is the quota higher?
Yes, but not infinite. Workspace quotas are higher (1,500 emails vs. 100, 100,000 UrlFetch calls vs. 20,000), but a script calling UrlFetch inside an hourly trigger over a large dataset can still exhaust even the Workspace ceiling. Check Apps Script > Quotas in the left nav of script.google.com to see your exact remaining quota in real time.
Can I catch the error and retry automatically?
You can catch it with a try/catch around the offending call and log or queue the failure, but retrying immediately will just throw the same error. The quota is already exhausted. The right response is to abort the current run, store progress in PropertiesService, and let the next scheduled trigger pick up where you left off.
MailApp vs. GmailApp — do they share the same quota?
Yes. Both draw from the same daily email quota for your account type. Switching from GmailApp.sendEmail to MailApp.sendEmail does not give you more sends; it is the same underlying limit. The practical difference between the two is that MailApp cannot read your inbox, so it requires fewer OAuth scopes — useful if you want a tighter permission profile, but not a quota workaround.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.