Caching Strategies
Use HTTP caching headers, CDN edge caching, and app-level caches (Redis, in-memory) to reduce SheetsAPI calls and speed up your application.
SheetsAPI reads your Google Sheet on every request. If your sheet changes infrequently, caching responses can dramatically reduce latency and cut the number of API calls you make.
HTTP response headers
Every SheetsAPI response includes:
| Header | Typical value | Meaning |
|---|---|---|
Cache-Control | public, max-age=60 | Clients and CDNs may cache for 60 s |
ETag | "abc123" | Fingerprint of the response body |
Vary | Authorization | Separate caches for authenticated vs. public |
Conditional requests with ETag
Re-use the ETag from a prior response to avoid re-downloading unchanged data:
let etag: string | null = null;
let cachedData: unknown[] = [];
async function fetchWithCache(url: string) {
const headers: Record<string, string> = {};
if (etag) headers["If-None-Match"] = etag;
const resp = await fetch(url, { headers });
if (resp.status === 304) {
// Not modified - return the data we already have
return cachedData;
}
etag = resp.headers.get("ETag");
cachedData = (await resp.json()).data;
return cachedData;
}A 304 Not Modified response has no body and counts against your rate limit at a much
lower cost than a full 200 response.
CDN / edge caching (public sheets)
For public sheets (no Authorization header), responses carry Cache-Control: public
and can be cached at the CDN edge. If you are self-hosting behind Cloudflare, Fastly, or
a similar CDN, these responses are cached automatically.
To extend the TTL further, add a Cache-Control override in your CDN rules:
Cache-Control: public, s-maxage=300, stale-while-revalidate=60
This serves stale data for up to 60 seconds while the CDN revalidates in the background - no user ever waits for a cold cache.
In-memory cache (Node.js / Bun)
A simple TTL cache avoids hitting the network on every request:
interface CacheEntry<T> {
data: T;
expiresAt: number;
}
const cache = new Map<string, CacheEntry<unknown>>();
async function cachedFetch<T>(url: string, ttlMs = 60_000): Promise<T> {
const now = Date.now();
const entry = cache.get(url);
if (entry && entry.expiresAt > now) {
return entry.data as T;
}
const resp = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SHEETS_API_KEY}` },
});
const data = (await resp.json()) as T;
cache.set(url, { data, expiresAt: now + ttlMs });
return data;
}Usage:
const products = await cachedFetch<{ data: Product[] }>(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products?limit=50",
);Memory growth: The
Mapabove is unbounded. For production, cap the cache size or use a proper LRU implementation such aslru-cache.
Redis cache (multi-instance / serverless)
For serverless functions or apps running on multiple instances, a shared Redis cache keeps all instances in sync:
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function getFromSheets<T>(userKey: string, sheet: string, ttl = 60): Promise<T> {
const key = `sheets:${userKey}:${sheet}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit) as T;
const resp = await fetch(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${userKey}/${sheet}?limit=100`,
{ headers: { Authorization: `Bearer ${process.env.SHEETS_API_KEY}` } },
);
const data = (await resp.json()) as T;
await redis.setEx(key, ttl, JSON.stringify(data));
return data;
}Cache invalidation
Invalidate the cache when you know the sheet has changed (e.g. after a POST that adds a row):
async function addRow(userKey: string, sheet: string, row: Record<string, string>) {
await fetch(`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${userKey}/${sheet}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SHEETS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(row),
});
// Invalidate so next read fetches fresh data
await redis.del(`sheets:${userKey}:${sheet}`);
}Next.js App Router (fetch cache)
Next.js 15 caches fetch calls during static generation. Opt in explicitly and set a
revalidation interval:
// app/products/page.tsx
export const revalidate = 60; // ISR: regenerate page every 60 seconds
async function getProducts() {
const resp = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products?limit=50",
{
headers: { Authorization: `Bearer ${process.env.SHEETS_API_KEY}` },
next: { revalidate: 60 },
},
);
return resp.json();
}For on-demand revalidation, call revalidatePath("/products") or revalidateTag("products")
from a Route Handler after the sheet is updated.
Choosing a caching layer
| Scenario | Recommended approach |
|---|---|
| Static marketing site | CDN edge cache + ISR (revalidate) |
| SSR / API route, single server | In-memory Map with TTL |
| Serverless / multi-instance | Redis with setEx TTL |
| High read / low write | Conditional requests (ETag) + local cache |
| Real-time dashboard | No cache - use polling instead |
Summary
- SheetsAPI sends
ETagandCache-Controlheaders on every response. - Use
If-None-Matchto get304 Not Modifiedand skip re-downloading. - For public sheets, CDN edge caching is free and automatic.
- Use an in-memory or Redis cache for private sheets in server-side code.
- Next.js ISR +
fetchcache is the easiest full-page caching option. - Invalidate the cache immediately after writes to avoid serving stale data.