Chapter 4. Living with the Quotas
One of the daily irritations of Apps Script is being caught by quota limitations that cause scripts to fail. Quotas are imposed on many Apps Script services because they run on shared infrastructure. Runaway processes could have a serious impact on other Apps Script users, and this quota system is intended to protect the Apps Script community as a whole, even though individual users might be occasionally inconvenienced.
Another good reason for quotas is as a signal that an application is no longer suitable for the Apps Script environment, and should be moved to a paid and more scalable environment such as App Engine.
To VBA users, who have free reign over the local PC resources, this can come as somewhat of a culture shock, because these quotas deeply affect the approach to application structure. Although VBA has no direct corollary to this problem, it is important to understand how quotas affect Apps Script development.
The chapter looks at some ways to live with quotas, rate limits, and what Google calls limitations.
The Quotas
Many of the underlying APIs used by Apps Script have their own quotas, but Apps Script often has additional restrictions.
Daily Limits
Some services allow you to perform a maximum number of a particular operation per day; that is, when you exhaust this quota, you can no longer perform that specific operation that day. Table 4-1 lists the daily limits from the Apps Script dashboard at the time of writing.
Action | Consumer (gmail.com) |
Google Apps for your domain |
Google Apps for biz/edu/gov |
---|---|---|---|
Calendar events created | 5,000 | 10,000 | 10,000 |
Contacts created | 1,000 | 2,000 | 2,000 |
Documents created | 250 | 500 | 1,500 |
Email recipients per day | 100 | 100 | 1,500 |
Gmail read/write (excluding send) | 20,000 | 40,000 | 50,000 |
Groups read | 2,000 | 5,000 | 10,000 |
JDBC connection | 10,000 | 10,000 | 50,000 |
JDBC failed connection | 100 | 100 | 500 |
Property GET or SET | 50,000 | 100,000 | 500,000 |
SOAP calls | 5,000 | 20,000 | 20,000 |
Spreadsheets created | 250 | 500 | 3,200 |
Triggers total runtime | 1 hours | 3 hours | 6 hours |
URLFetch calls | 20,000 | 50,000 | 100,000 |
URLFetch data received | 100 MB | 100 MB | 100 MB |
Limitations
Another type of quota is known as a limitation by Google. Typically, these are size limitations for an operation’s maximum payload. These are hard limits, and there is not much that can be done other than to design applications to avoid hitting them. Table 4-2 lists the limitations at the time of this writing.
Feature | Consumer (gmail.com) | Google Apps for your domain | Google Apps for biz/edu/gov |
---|---|---|---|
Script runtime | 6 min/execution | 6 min/execution | 6 min/execution |
Email attachments | 250/msg | 250/msg | 250/msg |
Email body size | 200 KB/msg | 200 KB/msg | 400 KB/msg |
Email recipients per message | 50/msg | 50/msg | 50/msg |
Email total attachments size | 25 MB/msg | 25 MB/msg | 25 MB/msg |
Properties value size | 9 KB/val | 9 KB/val | 9 KB/val |
Properties total storage | 500 KB/property store | 500 KB/property store | 500 KB/property store |
Triggers | 20/user/script | 20/user/script | 20/user/script |
URL Fetch headers | 100/call | 100/call | 100/call |
URL Fetch header size | 8 KB/call | 8 KB/call | 8 KB/call |
URL Fetch POST size | 10 MB/call | 10 MB/call | 10 MB/call |
URL Fetch URL length | 2 KB/call | 2 KB/call | 2 KB/call |
Triggers
Being able to run processes at predetermined times is a great feature of Apps Script.
VBA provides Application.Ontime, and you can also use the Windows scheduler for this purpose, but in both cases, your PC needs to be on, and in the case of Application.Ontime it needs to be running Office.
Apps Script Triggers have some limitations (mentioned in Tables 4-1 and 4-2), but they also are less specifically controllable than their VBA equivalents.
The Trigger dialog is shown in Figure 4-1.
The times at which these scripts will actually run are only loosely estimated, and as a result they are not predictable enough to be able to use for scheduling dependent processes.
Triggers also run at a reduced priority, so processes that run interactively within the six-minute execution limit may take longer when executed as a trigger.
It’s important to remember that the six-minute execution window is elapsed time (not compute time).
Rate Limits
Rate limits control how rapidly certain operations can be executed, and in some ways are the trickiest to work around. The documentation for exactly which operations are subject to rate limits is hard to track down, but the consequence of hitting one is this error message:
Service invoked too many times in a short time
Throttling
Throttling is slowing down, or adapting a script’s demand profile to operate within the resource’s servicing capability.
Sleeping
The simple way to throttle scripts that might be affected by rate limits is simply to sleep after each operation. The examples in the Apps Script documentation are usually based on this approach, and usually show something like this (this example is getting all the messages associated with a Gmail thread):
JS
threads
.
forEach
(
function
(
d
)
{
var
messages
=
d
.
getMessages
();
doSomeThingWith
(
messages
);
Utilities
.
sleep
(
1000
);
});
Although this is certainly the simplest approach, it could generate a lot of unnecessary waiting. Assuming that the rate limit on getting messages from Gmail is 1 per second, the actual time to return the getMessages
and the doSomethingWith
functions is going to be affected by the resources available at that time, in addition to their actual execution times.
One second may even already have elapsed by the time the sleep request is executed, meaning that the request might not even be needed. On the other hand, the response could be almost instantaneous, in which case not sleeping between calls risks script failure.
In reality, although it’s not documented, the rate doesn’t appear to be checked every second, but instead averaged over a longer period of time. This means that short bursts of activity can get away with more rapid operations than the rate limit implies, so there is all the more reason to avoid unnecessary idle time.
Sleeping counts toward the six-minute execution time, so a script that reads 200 threads from Gmail (a typical day’s worth of mail) would spend more than half of its available execution time doing nothing.
Exponential Backoff
Exponential backoff attempts the operation, and if it fails, waits a bit, then tries again until it succeeds or gives up. This means that waiting happens only if an error is signaled. The amount of wait time on each attempt:
-
Increases exponentially to allow the clearing of any blockage related to infrastructure overload or rate limit averages measured over a period of time.
-
Includes a random factor to avoid multiple processes getting locked into the same retry schedule.
Because this is such a common requirement in almost every script that accesses rate-limited resources, it’s a good idea to use a common library function for backoff.
It is used like this, with the function to repeatedly attempt being passed anonymously:
JS
threads
.
forEach
(
function
(
d
)
{
var
messages
=
Utils
.
rateLimitExpBackoff
(
function
()
{
return
d
.
getMessages
();
};
doSomeThingWith
(
messages
);
});
Exponential backoff waits only if there was a rate limited error signaled, then tries again. This minimizes both idle time and unpredictable failures.
Code for exponential backoff
The error messages can be internationalized for the kinds of errors that should be retried. An example of a quota error is given in Russian in the following snippet. To add your own language, just expand the list with the equivalent Apps Script error message.
These are the errors that have shown to be recoverable on retry:
JS
utils
.
tryAgain
=
"force backoff anyway"
;
utils
.
backoffErrors
=
[
"Exception: Service invoked too many times"
,
"Exception: Rate Limit Exceeded"
,
"Exception: Quota Error: User Rate Limit Exceeded"
,
"Service error: Spreadsheets"
,
"Exception: User rate limit exceeded"
,
"Exception: Internal error. Please try again."
,
"Exception: Cannot execute AddColumn because another task"
,
"Service invoked too many times in a short time:"
,
"Exception: Internal error."
,
"Exception: Превышен лимит: DriveApp."
,
utils
.
tryAgain
];
utils
.
rateLimitExpBackoff
=
function
(
callBack
,
sleepFor
,
maxAttempts
,
attempts
,
optLogAttempts
,
optChecker
)
{
// can handle multiple error conditions by expanding this list
function
errorQualifies
(
errorText
)
{
return
utils
.
backoffErrors
.
some
(
function
(
e
){
return
errorText
.
toString
().
slice
(
0
,
e
.
length
)
==
e
;
});
}
// sleep start default is 1 second
sleepFor
=
Math
.
abs
(
sleepFor
||
1000
);
// attempt number
attempts
=
Math
.
abs
(
attempts
||
1
);
// maximum tries before giving up
maxAttempts
=
Math
.
abs
(
maxAttempts
||
5
);
// make sure that the checker is really a function
if
(
optChecker
&&
typeof
(
callBack
)
!==
"function"
)
{
throw
(
"if you specify a checker it must be a function"
);
}
// check properly constructed
if
(
!
callBack
||
typeof
(
callBack
)
!==
"function"
)
{
throw
(
"you need to specify a function for rateLimitBackoff to execute"
);
}
// try to execute it
else
{
try
{
var
r
=
callBack
();
// this is to find content based errors that might benefit from a retry
return
optChecker
?
optChecker
(
r
)
:
r
;
}
catch
(
err
)
{
if
(
optLogAttempts
)
Logger
.
log
(
"backoff "
+
attempts
+
":"
+
err
);
// failed due to rate limiting?
if
(
errorQualifies
(
err
))
{
//give up?
if
(
attempts
>
maxAttempts
)
{
throw
(
err
+
" (tried backing off "
+
(
attempts
-
1
)
+
" times"
);
}
else
{
// wait for some amount of time based on how many times we've tried
// plus a small random bit to avoid races
Utilities
.
sleep
(
Math
.
pow
(
2
,
attempts
)
*
sleepFor
)
+
(
Math
.
round
(
Math
.
random
()
*
sleepFor
));
// try again
return
rateLimitExpBackoff
(
callBack
,
sleepFor
,
maxAttempts
,
attempts
+
1
,
optLogAttempts
);
}
}
else
{
// some other error
throw
(
err
);
}
}
}
};
Libraries
The Properties service is a secure store, rather like the Windows registry. Chapter 5 is dedicated entirely to this service. Scripts each come with their own Properties service. You can mitigate the limitation on overall property size by using the Properties service of a library and passing it to the main script(s) that use it:
JS
// library
function
getPropertyService
()
{
return
PropertiesService
.
getScriptProperties
();
}
// main script
function
work
()
{
var
libraryProps
=
Library
.
getPropertyService
();
var
mainProps
=
PropertiesService
.
getScriptProperties
();
}
Parallel Running
Similar to batching, a shared library Properties service can be used to orchestrate cooperating scripts, except in this case the scripts run simultaneously. Often you’ll use the trigger service to schedule simultaneous processes, occasionally using MapReduce-type techniques1 to consolidate and pass data from one phase to another.
Offloading
Apps Script runs on Google Servers, but HTML service tasks can be orchestrated by Apps Script to run on the local client. Generally speaking, compute-intensive tasks will run more quickly when executed locally. The HTML service can asynchronously fire off Apps Script tasks to deal with processes that require interaction with Apps Script services.
Carefully offloading work to the HTML service is a way of completely circumventing many of the server-side limitations, but it’s a complex topic that’s beyond the VBA/Apps Script scope.
Avoiding Service Calls
Many of the rate-limited services are related to fetching data externally or from Google services or APIs. In some cases, either the data will not have changed from one call to the next, or it is not critical that the very latest data be used. Apps Script provides a Cache service for rapid access to data, enabling the complete avoidance of quota-consuming service calls.
Cache Service
The Cache service allows data to be stored in key/value pairs. It is subject to limitations in data size, but is not constrained by rate limits. As a result, it’s often good practice to write service or external data that might be needed by this script to cache, and to first check the cache to see if it’s already there. Not only does this avoid service call quota consumption, but also cache is generally faster to access than Apps Script services or external data and doesn’t need to be reprocessed.
Cache scopes
The selection of which cache to use depends on the data visibility required. The cache visibility is limited to the current script, but within that there are three different caches, described in Table 4-3.
Cache | Brief description |
---|---|
DocumentCache |
Applies to the current document |
ScriptCache |
Applies to all users of this script |
UserCache |
Applies to the current user of this script |
A cache is obtained from the Cache service like so:
JS
var
cache
=
CacheService
.
getScriptCache
();
Data can be written like this:
JS
var
data
=
getDataFromSomeService
();
cache
.
put
(
"mykey"
,
data
);
A future call to this same script would first check to see if the data was already in the cache, and if not, make the service call, thus avoiding the service call if there had been one made recently:
JS
var
data
=
cache
.
get
(
"mykey"
);
if
(
!
data
)
{
data
=
getDataFromSomeService
();
cache
.
put
(
"mykey"
,
data
);
}
Cache data expiry
It’s important that cache data expires in order to provoke a refresh from time to time. By default, the cache will expire after 10 minutes, but you can change that like so (where the third parameter is the expiry time in seconds):2
JS
cache
.
put
(
"mykey"
,
data
,
1200
);
Sharing cache
Caches are scoped to a particular script, but there may be occasions when multiple scripts would want to benefit from each other’s cached results. You cannot share caches directly, but you can make them accessible through a shared library, using the same technique we used earlier for sharing the Properties services:
JS
// Library
function
getCacheService
()
{
return
CacheService
.
getScriptCache
();
}
// main script
var
cache
=
Library
.
getCacheService
();
var
data
=
cache
.
get
(
"mykey"
);
if
(
!
data
)
{
data
=
getDataFromSomeService
();
cache
.
put
(
"mykey"
,
data
);
}
1 MapReduce is a technique where a workload is split into chunks (Map) and processed in parallel. The outputs from each thread are combined (Reduce) into a consolidated result. Examples of Apps Script implementations of MapReduce are covered on the Desktop Liberation site.
2 It is not guaranteed that the cache will last that long, as actual cache lifetime can be affected by resource availability in the Google infrastructure.
Get Going GAS now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.