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.
CacheServiceis 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
cachedFetchtakes the same arguments asUrlFetchApp.fetch, plus a TTL. That makes it a near drop-in replacement — change the function name and you are done.- 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.
- It checks the script cache. A hit returns the stored response body straight away — no network call, sub-second.
- On a miss it makes the real request and reads the body.
- It only caches successful (2xx) responses. Caching an error would freeze the failure in place for the whole TTL; better to retry next time.
- Responses over 100KB are returned but not cached — that is the hard
CacheServicelimit 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:
| Open | Source | Round-trip |
|---|---|---|
| 1 | Network (cache miss) | ~600 ms |
| 2 | Cache | ~15 ms |
| 3 | Cache | ~12 ms |
| 4 | Cache | ~14 ms |
| 5 | Cache | ~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:
- Paste the function into the project that makes API calls.
- Find each
UrlFetchApp.fetch(...)and decide how stale that data may be. - Replace the call with
cachedFetch(url, options, ttlSeconds), passing a TTL that matches — short for fast-moving data, long for near-static feeds. - Leave calls that must always be live (writes, payments, anything
non-idempotent) on plain
UrlFetchApp.fetch.
Watch out for
CacheServiceentries 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
Handle streaming responses from an LLM API
Manage long Northwind AI outputs reliably — note: Apps Script UrlFetch is synchronous.
Updated Jan 3, 2026
Build an API-key vault and rotation system
Manage Northwind credentials securely at scale — centralised storage, scheduled rotation.
Updated Dec 22, 2025
Build a rate-limit-aware API client
Back off and retry gracefully on 429s — Northwind's robust outbound HTTP pattern.
Updated Dec 14, 2025
Build a generic paginated-API fetcher
Handle cursors and pages for any large dataset — Northwind's standard pull pattern.
Updated Dec 6, 2025
Add carrier rate and shipping cost lookups
Quote Northwind shipping inline from a carrier API — DHL or UPS rates per order.
Updated Dec 2, 2025