Understand and design around Apps Script quotas
Limits, costs, and how Northwind scripts stay under them at scale.
Published Jul 7, 2025
Apps Script is free, and the price of free is quotas. Google caps how much email a script can send, how many external calls it can make, and how long it can run — every day, per account. Small scripts never notice. Scripts that process real volume hit a wall, usually mid-run, usually without warning.
The good news is that quotas are predictable. They are published, they are the same every day, and you can check the most important ones live from inside the script. Designing around them is mostly a matter of knowing the numbers and shaping the work to fit. This guide covers the limits that matter and the patterns that keep scripts under them.
Daily quotas (consumer Workspace)
These are the limits that bulk scripts run into most often. They reset on a rolling 24-hour basis, not at midnight.
| Limit | Free Gmail | Workspace |
|---|---|---|
| Email recipients per day | 100 | 1,500 |
UrlFetchApp calls per day | 20,000 | 100,000 |
| Trigger runtime per day | 6 hours total | 6 hours total |
| Script runtime per execution | 6 minutes | 6 minutes (30 for editor runs) |
A few details that catch people out:
- Email is counted per recipient, not per message. One email to a 50-person list costs 50 of the daily allowance.
- Trigger runtime is a total across all triggers, not per trigger. Many small scheduled jobs share the same 6-hour pool.
- The per-execution limit is hard. When a single run hits 6 minutes it is killed where it stands, leaving the work half-finished.
Hitting them
Some quotas can be checked live before you spend them, which lets a script bail out gracefully instead of failing partway. Email is the easiest one to query.
// getRemainingDailyQuota() returns how many recipients can still be emailed
// today. Stop the run before it fails rather than after.
if (MailApp.getRemainingDailyQuota() < 10) {
throw new Error('Out of quota');
}
Checking up front means a quota shortfall becomes a clean, early error with a clear message — not a job that dies on recipient 91 of 200 and leaves the rest unsent with no record of where it stopped.
Not every quota has a live check. Execution time and UrlFetchApp calls have no
“remaining” accessor, so for those you design the work to stay safely under the
limit rather than querying it.
Designing around them
The reliable way to handle quotas is to assume you will approach them and shape the work accordingly. Three patterns cover almost every case.
- Pace bulk work. Spread sends and calls over time instead of bursting them. This keeps you under per-day and per-minute limits and avoids spam-filter scrutiny — see Throttle bulk sends to stay under Gmail quotas for the throttling pattern.
- Cache external calls. A response that does not change often should be
fetched once and reused. Cache API responses
keeps
UrlFetchAppusage low by serving repeat requests from cache. - Chunk long jobs. Work that cannot finish in six minutes must be broken into pieces, each one a separate run that picks up where the last left off. Process large datasets without timeouts shows how to split a job and resume it.
Together these address the three quota dimensions: pacing handles per-day caps, caching handles call counts, and chunking handles the per-execution time limit.
Common mistakes
- Treating quotas as edge cases. On any real workload they are the normal case — design for them from the start rather than patching after the first failure.
- Counting emails by message instead of by recipient, then being surprised when a single send to a large list exhausts the daily allowance.
- Assuming the 6-minute limit can be stretched. It cannot — long jobs must be chunked into separate runs, not optimised down to fit.
- Forgetting that trigger runtime is pooled. A dozen small hourly triggers can together exhaust the 6-hour daily budget.
- Not checking
getRemainingDailyQuota()before a bulk send, so the script discovers the shortfall mid-run and leaves recipients in an inconsistent state.