Google Apps Script enforces daily quotas (email recipients, spreadsheets created, trigger runtime) and per-execution limits (6-minute script runtime, 20 triggers per script) that vary by account type. Hit one and your script throws an exception mid-run instead of failing gracefully — usually Service invoked too many times or Service using too much computer time for one day. Below is the complete table straight from Google’s documentation, verified today, plus a script that checks your usage before you hit the wall instead of after.
Daily quotas: Google Workspace services
These reset 24 hours after your script’s first request that day, not at midnight. Consumer accounts (gmail.com) get the lower column; Google Workspace accounts get the higher one. Verified against Google’s official quotas page on August 24, 2026 (the page’s own last-updated stamp reads July 22, 2026, so this is current).
| Feature | Consumer accounts | Workspace accounts |
|---|---|---|
| Calendar events created | 5,000 / day | 10,000 / day |
| Contacts created | 1,000 / day | 2,000 / day |
| Documents created | 250 / day | 1,500 / day |
| Files converted | 2,000 / day | 4,000 / day |
| Email recipients per day (MailApp) | 100 / day | 1,500 / day |
| Email recipients per day, within domain | 100 / day | 2,000 / day |
| Email read/write (excluding send) | 20,000 / day | 50,000 / day |
| Groups read | 2,000 / day | 10,000 / day |
| JDBC connections | 10,000 / day | 50,000 / day |
| JDBC failed connections | 100 / day | 500 / day |
| Presentations created | 250 / day | 1,500 / day |
| Properties read/write | 50,000 / day | 500,000 / day |
| Slides created | 250 / day | 1,500 / day |
| Spreadsheets created | 250 / day | 3,200 / day |
| Triggers total runtime | 90 min / day | 6 hr / day |
| URL Fetch calls | 20,000 / day | 100,000 / day |
| Translate calls | 5,000 / day | 20,000 / day |
| Apps Script projects (creating new ones) | 50 / day | 50 / day |
The two rows that actually break Sheets automations are email recipients per day and triggers total runtime — both show up repeatedly in the scripts on this site (see the daily email digest script, which runs into the email quota if you point it at a big distribution list).
Per-execution limits
These aren’t daily totals — they cap what a single run is allowed to do, and they don’t reset until that execution ends.
| Feature | Consumer accounts | Workspace accounts |
|---|---|---|
| Script runtime | 6 min / execution | 6 min / execution |
| Custom function runtime (in a cell formula) | 30 sec / execution | 30 sec / execution |
| Simultaneous executions per user | 30 / user | 30 / user |
| Simultaneous executions per script | 1,000 | 1,000 |
| Triggers | 20 / user / script | 20 / user / script |
| Email recipients per message | 50 / msg | 50 / msg |
| Email attachments | 250 / msg | 250 / msg |
| Email body size | 200 KB / msg | 400 KB / msg |
| Email total attachments size | 25 MB / msg | 25 MB / msg |
| Properties value size | 9 KB / value | 9 KB / value |
| Properties total storage | 500 KB / store | 500 KB / store |
| URL Fetch response size | 50 MB / call | 50 MB / call |
| URL Fetch POST size | 50 MB / call | 50 MB / call |
| Script versions | 200 / script | 200 / script |
The one people miss: “20 triggers per user per script” counts every installable trigger you’ve added to that project — onEdit, onFormSubmit, and every time-driven trigger you created while testing. If you cloned a project and re-ran the setup function three times, you may already be at 15–18 without realizing it. Apps Script’s editor (Triggers panel, left sidebar) is the only reliable way to see the real count.
Which limit will you hit first?
The quota that bites depends on what kind of automation you’re running, not on how “big” your script is.
| What you’re building | Limit most likely to hit | Guide on this site |
|---|---|---|
| Daily email digest / summary | Email recipients per day (100 or 1,500) | Email Yourself a Daily Google Sheets Summary |
| onEdit trigger firing on every keystroke/paste | Simultaneous executions per user (30) and triggers total runtime | Send an Email When a Cell Changes |
| Chained time-driven triggers (several per day) | Triggers total runtime (90 min / 6 hr) and the 20-trigger cap | — |
| Bulk row copy or Drive file processing | Script runtime (6 min per execution) — needs batching across multiple triggered runs | — |
| Gmail attachment scraping into Drive/Sheets | Email read/write (20,000 / 50,000 per day) | Save Gmail Attachments to Drive |
| =AI() formulas across a large range | Custom function runtime (30 sec) and the service’s own 350-cell batch cap | Google Sheets =AI() Function |
What the error messages actually mean
These are the exact strings Google’s documentation lists, not paraphrased.
| Error text | What it means | Fix |
|---|---|---|
Limit exceeded: Email Attachments Per Message. |
A per-execution limit was crossed (see the second table) | Check which specific limit — the error names it |
Service invoked too many times: Calendar. |
You called that service more times than the daily quota allows | Batch the calls, or spread work across days with PropertiesService tracking (see script below) |
Service invoked too many times in a short time: Calendar. Try Utilities.sleep(1000) between calls. |
Same service called too fast, not necessarily too often | Add Utilities.sleep(1000) between calls in a loop |
Service using too much computer time for one day. |
Cumulative trigger runtime for the day exceeded 90 min (consumer) / 6 hr (Workspace) | Reduce trigger frequency, or use the runtime-budget guard below to skip a run instead of crashing |
Script invoked too many times per second for this Google user account. |
Too many separate executions started within a short window | Stagger multiple triggers so they don’t fire in the same second |
A script that stops the failure before it happens
Most quota advice tells you what the limits are after you’ve already been throttled. This does the check first: it stores a running daily total in PropertiesService and refuses to proceed — returning a clear reason instead of throwing — once a run would cross the limit. Two guards: one for email recipients, one for cumulative trigger runtime.
/**
* Quota-budget guard for Apps Script triggers.
* Prevents "Service invoked too many times: Email" and
* "Service using too much computer time for one day" by tracking
* cumulative usage in Script Properties and refusing to proceed
* once a run would cross the daily limit.
*
* Paste into Apps Script, call trackEmailQuota()/trackTriggerRuntimeBudget()
* before the risky operation, and skip/return early if allowed/shouldSkip is false.
*/
var EMAIL_DAILY_LIMIT = 100; // 100 consumer / 1500 Workspace - see quota table
var TRIGGER_MINUTES_LIMIT = 90; // 90 consumer / 360 (6 hr) Workspace
function todayKey_(now) {
return Utilities.formatDate(now, Session.getScriptTimeZone(), 'yyyy-MM-dd');
}
function trackEmailQuota(recipientsThisRun, dailyLimit, store, now, warnAtFraction) {
if (typeof warnAtFraction !== 'number') warnAtFraction = 0.9;
if (recipientsThisRun < 0) throw new Error('recipientsThisRun must be >= 0');
if (dailyLimit <= 0) throw new Error('dailyLimit must be > 0');
var key = todayKey_(now);
var raw = store.getProperty('EMAIL_QUOTA_' + key);
var usedSoFar = raw ? parseInt(raw, 10) : 0;
if (isNaN(usedSoFar)) usedSoFar = 0;
var projected = usedSoFar + recipientsThisRun;
if (projected > dailyLimit) {
return {
allowed: false,
used: usedSoFar,
remaining: Math.max(dailyLimit - usedSoFar, 0),
projected: projected,
warning: null,
reason: 'Sending ' + recipientsThisRun + ' more would exceed the ' + dailyLimit + '/day limit (already used ' + usedSoFar + ').'
};
}
store.setProperty('EMAIL_QUOTA_' + key, String(projected));
var warning = null;
if (projected >= dailyLimit * warnAtFraction) {
warning = 'Email quota at ' + Math.round((projected / dailyLimit) * 100) + '% (' + projected + '/' + dailyLimit + ') for ' + key + '.';
}
return { allowed: true, used: projected, remaining: dailyLimit - projected, projected: projected, warning: warning, reason: null };
}
function trackTriggerRuntimeBudget(estimatedMinutesThisRun, dailyLimitMinutes, store, now) {
if (estimatedMinutesThisRun < 0) throw new Error('estimatedMinutesThisRun must be >= 0');
var key = todayKey_(now);
var raw = store.getProperty('TRIGGER_MIN_' + key);
var usedSoFar = raw ? parseFloat(raw) : 0;
if (isNaN(usedSoFar)) usedSoFar = 0;
var projected = usedSoFar + estimatedMinutesThisRun;
var shouldSkip = projected > dailyLimitMinutes;
if (!shouldSkip) {
store.setProperty('TRIGGER_MIN_' + key, String(projected));
}
return { shouldSkip: shouldSkip, usedMinutes: shouldSkip ? usedSoFar : projected, remainingMinutes: Math.max(dailyLimitMinutes - (shouldSkip ? usedSoFar : projected), 0) };
}
/** Example: guard a daily digest email before sending. */
function sendDailyDigestGuarded() {
var store = PropertiesService.getScriptProperties();
var recipients = ['a@example.com', 'b@example.com']; // build this list from your sheet
var quota = trackEmailQuota(recipients.length, EMAIL_DAILY_LIMIT, store, new Date());
if (!quota.allowed) {
Logger.log(quota.reason);
return; // skip this run instead of throwing mid-script
}
if (quota.warning) {
Logger.log(quota.warning);
}
MailApp.sendEmail(recipients.join(','), 'Daily digest', 'Body goes here');
Logger.log('Remaining email quota today: ' + MailApp.getRemainingDailyQuota());
}
/** Example: skip a scheduled batch job if it would blow the trigger-runtime budget. */
function runBatchJobGuarded() {
var store = PropertiesService.getScriptProperties();
var estimatedMinutes = 3; // rough estimate for this job
var budget = trackTriggerRuntimeBudget(estimatedMinutes, TRIGGER_MINUTES_LIMIT, store, new Date());
if (budget.shouldSkip) {
Logger.log('Skipping run - only ' + budget.remainingMinutes + ' trigger minutes left today.');
return;
}
// ... do the batch work ...
}
The design decision worth explaining: on exceeding the limit, trackEmailQuota() does not write the overage back to PropertiesService. Without that guard, a rejected run would still bump the stored counter, permanently under-reporting how much quota is actually left. The test suite below checks this exact case.
How to check usage without waiting for the error
- Email quota: call
MailApp.getRemainingDailyQuota()anywhere in a script — it returns the exact number of recipients you have left today, no PropertiesService needed for this one specifically. - Execution history: the Apps Script dashboard‘s “My Executions” page shows Completed/Failed/Running status per run; filter by Running to see simultaneous executions.
- Service-level API quotas: if the project is attached to a standard Google Cloud project, the Cloud console API dashboard breaks down usage per underlying API.
Free trial accounts have lower limits
Google’s documentation notes an easy-to-miss detail: Workspace accounts on a free trial get reduced limits until the domain has cumulatively paid at least $100 (or local equivalent) and at least 60 days have passed since hitting that threshold. If a script that worked fine in testing suddenly quota-errors after your org converts from trial to paid, this reset window is usually why — the increase isn’t immediate.
When this reference doesn’t apply to you
If your script runs once a day on a few dozen rows, none of these numbers matter — you’d need to be off by two or three orders of magnitude to notice. This page is for the scripts that fan out: sending to a real distribution list, processing every row in a large sheet on every edit, or chaining several time-driven triggers together. If you’re not sure which category you’re in, the Sheets automation checklist maps common tasks to the right method before quotas become a factor at all.
What I verified, and what I couldn’t
Verified today (2026-08-24): every number in both tables was fetched directly from Google’s official quotas documentation page, not copied from a third-party summary. The four exception message strings are quoted verbatim from the same page. The guard script’s logic (12 assertions: cumulative tracking, boundary conditions, day rollover, corrupted-value handling, overage not persisting, and the consumer-vs-Workspace runtime comparison) was run and passed in a Node.js sandbox using a mock PropertiesService, and the exact code published above passed Apps Script’s node --check syntax validation.
Not verified: I don’t have a Google Workspace account, so I could not trigger an actual quota exception, confirm MailApp.getRemainingDailyQuota()‘s real return value, or watch PropertiesService.getScriptProperties() behave under Apps Script’s real execution environment (as opposed to the mock used in testing). The mock reproduces the documented API surface (getProperty/setProperty) but can’t catch platform-specific quirks the docs don’t mention.
FAQ
Do quotas reset at midnight?
No. They reset 24 hours after your script’s first request of that “day” — a rolling window tied to when you first used the service, not a fixed clock time.
Does hitting a quota disable my whole Google account?
No. Only that specific service (Email, Calendar, etc.) stops responding to that script until the quota resets; other Google services and other scripts are unaffected.
Do time-driven triggers count against the 20-trigger limit the same as onEdit triggers?
Yes — the 20/user/script cap counts every installable trigger type together (onEdit, onFormSubmit, time-driven), not 20 of each kind separately.