appscript.dev
Automation Intermediate

Cache API responses to cut quota usage

Store and reuse Northwind API responses intelligently — sub-second hits, fewer bills.

Published Dec 26, 2025

Northwind’s dashboards refresh on every open, and each refresh fires the same handful of API calls — an exchange-rate lookup, a product feed, a status endpoint. The data barely changes hour to hour, but the calls go out every time anyway. That is slow for whoever opened the dashboard and, on metered APIs, it is a bill that grows for no reason.

This is a drop-in replacement for UrlFetchApp.fetch. Call cachedFetch instead and the first request hits the network; every identical request for the next hour is served from the script cache in milliseconds. Same arguments, same return value — you just stop paying for repeats.

What you’ll need

  • Any Apps Script project that already makes outbound API calls with UrlFetchApp.
  • Nothing to install. CacheService is built in and needs no authorisation.
  • A sense of how fresh each endpoint’s data needs to be — that is the one number you tune per call.

The script

// Default cache lifetime, in seconds, when a caller does not specify one.
const DEFAULT_TTL_SECONDS = 3600; // one hour

// Prefix on every cache key so HTTP cache entries are easy to spot.
const HTTP_CACHE_PREFIX = 'http:';

/**
 * A caching wrapper around UrlFetchApp.fetch. The first call for a given
 * URL-and-options pair hits the network; identical calls within the TTL
 * are served from the script cache.
 *
 * @param {string} url          The URL to fetch.
 * @param {Object} options      UrlFetchApp options (method, headers, etc.).
 * @param {number} ttlSeconds   How long to keep the response cached.
 * @returns {string}            The response body as text.
 */
function cachedFetch(url, options = {}, ttlSeconds = DEFAULT_TTL_SECONDS) {
  if (!url) throw new Error('cachedFetch needs a URL.');

  // 1. Build a cache key that is unique to this exact request.
  //    Hashing keeps the key short and within CacheService's key limit,
  //    and folds the options in so a POST is not confused with a GET.
  const fingerprint = url + '|' + JSON.stringify(options);
  const digest = Utilities.computeDigest(
    Utilities.DigestAlgorithm.SHA_1,
    fingerprint
  );
  const key = HTTP_CACHE_PREFIX + Utilities.base64Encode(digest);

  const cache = CacheService.getScriptCache();

  // 2. Cache hit — return the stored body without touching the network.
  const hit = cache.get(key);
  if (hit !== null) return hit;

  // 3. Cache miss — make the real request.
  const response = UrlFetchApp.fetch(url, options);
  const body = response.getContentText();

  // 4. Only cache successful responses. Caching a 500 would lock in
  //    the failure for the whole TTL.
  const code = response.getResponseCode();
  if (code >= 200 && code < 300) {
    // CacheService rejects values over 100KB — skip caching if too big.
    if (body.length < 100 * 1024) {
      cache.put(key, body, ttlSeconds);
    }
  }

  return body;
}

How it works

  1. cachedFetch takes the same arguments as UrlFetchApp.fetch, plus a TTL. That makes it a near drop-in replacement — change the function name and you are done.
  2. It builds a cache key by combining the URL and the options into one string, hashing it with SHA-1, and base64-encoding the result. The hash keeps the key short and unique, and folding the options in means a GET and a POST to the same URL get separate cache entries.
  3. It checks the script cache. A hit returns the stored response body straight away — no network call, sub-second.
  4. On a miss it makes the real request and reads the body.
  5. It only caches successful (2xx) responses. Caching an error would freeze the failure in place for the whole TTL; better to retry next time.
  6. Responses over 100KB are returned but not cached — that is the hard CacheService limit on a single value.

Example run

Swap UrlFetchApp.fetch for cachedFetch in a dashboard helper:

function getExchangeRates() {
  // Rates change slowly — a 1-hour cache is plenty.
  const body = cachedFetch('https://api.example.com/rates', {}, 3600);
  return JSON.parse(body);
}

Open the dashboard five times in an hour:

OpenSourceRound-trip
1Network (cache miss)~600 ms
2Cache~15 ms
3Cache~12 ms
4Cache~14 ms
5Cache~13 ms

One API call instead of five. Across a team and a working day that is the difference between thousands of metered calls and a few dozen.

Run it

There is nothing to schedule — cachedFetch runs whenever your code calls it. To adopt it:

  1. Paste the function into the project that makes API calls.
  2. Find each UrlFetchApp.fetch(...) and decide how stale that data may be.
  3. Replace the call with cachedFetch(url, options, ttlSeconds), passing a TTL that matches — short for fast-moving data, long for near-static feeds.
  4. Leave calls that must always be live (writes, payments, anything non-idempotent) on plain UrlFetchApp.fetch.

Watch out for

  • CacheService entries are capped at 100KB per value. The script skips caching anything larger — for big responses, persist to a Sheet or a Drive file instead.
  • The cache is best-effort. Apps Script can evict entries before the TTL expires under memory pressure, so treat the TTL as a maximum, not a promise.
  • Script cache is shared across all users of the script. That is what makes it effective for shared dashboards, but never cache a response that contains per-user or private data, or one user will see another’s result.
  • Only cache idempotent reads. Wrapping a POST that creates or charges something will silently swallow the second call — keep writes uncached.
  • A stale cache hides upstream changes for up to the TTL. If a feed is wrong, shorten the TTL or clear the cache rather than waiting it out.
  • The key folds in the options object via JSON.stringify. Two requests that are logically identical but pass options in a different key order will get separate cache entries — keep your options objects consistent.

Related