Skip to content
// Apps Script

Cache API responses with CacheService in Apps Script.

How to use Apps Script CacheService to store API responses, avoid quota exhaustion, and work within the 6-hour TTL and 100KB-per-key hard limits.

I keep hitting external API rate limits or slow fetch times inside my Apps Script project and want to cache responses so I'm not making the same call on every execution.

The script

copy · paste · trigger
fetchWithCache.gs
Apps Script
// Fetch JSON from url; serve from cache when possible.
// TTL max is 21600 s (6 h); cache is advisory — check for null on reads.
function fetchWithCache(url, ttlSeconds) {
  var cache = CacheService.getScriptCache();
  var key = 'apicache_' + Utilities.computeDigest(
    Utilities.DigestAlgorithm.MD5,
    url,
    Utilities.Charset.UTF_8
  ).join('');

  var cached = cache.get(key);
  if (cached !== null) {
    return JSON.parse(cached);
  }

  var response = UrlFetchApp.fetch(url);
  var data = JSON.parse(response.getContentText());
  var slim = { result: data.items, fetchedAt: Date.now() };
  var payload = JSON.stringify(slim);
  if (payload.length < 100000) {
    cache.put(key, payload, Math.min(ttlSeconds, 21600));
  }
  return slim;
}

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

Walkthrough

Why the limits shape everything

CacheService stores strings only, caps each entry at roughly 100KB of string data, and enforces a hard 6-hour (21,600 seconds) TTL ceiling — passing a larger number to cache.put() silently clamps to 21,600. The service itself can evict entries before that ceiling for capacity reasons, which is why the contract is advisory: cache.get() returning null is not a bug, it's a normal path you must handle every time.

The 100KB ceiling applies to the serialized string, not the raw object. A response from a verbose API like Google Sheets or a paginated REST endpoint can easily blow past it. The practical fix is to cache the derived result rather than the raw payload — extract only the fields the script actually uses, serialize that, then check payload.length before calling cache.put(). Storing a 350KB raw response just silently fails with no error thrown; you only notice when every execution hits the network.

Choosing the right cache scope

CacheService exposes three caches: getScriptCache() (shared across all users running this script deployment), getUserCache() (per Google account, isolated), and getDocumentCache() (tied to an open document, useful in add-ons). For API responses that are the same for everyone, getScriptCache() is correct and gives the best hit rate. For user-personalized API calls — OAuth-protected endpoints where each user gets different data — getUserCache() prevents one user's stale data from poisoning another's view.

I keep a small wrapper that picks the scope based on a parameter rather than hardcoding it, because the same fetchWithCache pattern gets reused across scripts and the wrong choice there is a silent data-correctness bug, not a quota bug.

Keying, chunking, and invalidation

Using the raw URL as a cache key works until the URL contains tokens or timestamps that vary per call, in which case every invocation is a cache miss. Hashing the stable portion of the URL with Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, ...) gives a fixed-length key under the 250-character key length limit.

For payloads that genuinely must exceed 100KB, the pattern is manual chunking: split the JSON string into 90KB slices, store each under key + '_0', key + '_1', and so on, then store a manifest key that lists how many chunks exist. Reading requires fetching the manifest first. It is more code than it looks like, and for most scripts a better answer is rethinking which fields you actually need. True cache invalidation — forcing a miss before TTL — is just cache.remove(key); for script-scoped caches you can also call cache.removeAll([key1, key2]) in bulk.

One gotcha: CacheService.getScriptCache() is shared per deployment, not per script file. If you have two separate Apps Script projects deployed in the same Google Workspace domain, they do not share a cache. Conversely, all functions inside one project share the same script cache namespace, so key collisions across functions in the same project are real and a 'apicache_' prefix like the example uses is not just style — it matters.

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
What happens if I pass a TTL larger than 21600 to cache.put()?
The call silently clamps to 21,600 seconds. No error, no warning. If you need the entry to live longer than 6 hours, CacheService is the wrong tool; use PropertiesService or a Spreadsheet as a persistent store instead.
Why does cache.get() sometimes return null before the TTL expires?
CacheService is an in-memory distributed cache; Google can evict entries early under memory pressure. The cache is explicitly advisory. Always treat a null return as a legitimate path that triggers a fresh fetch, never as an error state.
Can I store non-string values like objects or arrays directly?
No. cache.put() accepts only strings. Anything else must be serialized with JSON.stringify() before storing and deserialized with JSON.parse() on read. Passing an object directly stores '[object Object]' and silently corrupts your data.
Is there a limit on how many keys I can store?
The documented per-script cache size is not publicly specified as a hard key count, but Google's quota page notes a 'size limit' that behaves like a shared pool. In practice, scripts that store hundreds of large entries start seeing unexpectedly early evictions. Keep entries small and count low; the cache is a hot-path accelerator, not a database.
// one good script a week

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