Caching a Third-Party API Response in a Cloudflare Pages Function
Build-time fetching handles most third-party data on a static site. It falls down in one specific case: content that should change on a schedule you are not rebuilding on. A page showing twelve rotating items each day, with a weekly deploy cadence, cannot get its data from the build.
The naive fix puts a fetch in the browser. Every visitor then hits the upstream API directly, which burns through rate limits, exposes any key you are using, and leaves the page empty when the provider has a bad afternoon.
A Pages Function sitting in front of the API fixes all three, and it is about twenty lines.
The Function
Drop this at functions/api/feed.js in your Pages project. It becomes available at /api/feed.
export async function onRequest(context) {
const cache = caches.default;
// Cache key changes once per day, which is what drives the rotation.
const day = new Date().toISOString().slice(0, 10);
const cacheKey = new Request(`https://cache.internal/feed/${day}`);
const hit = await cache.match(cacheKey);
if (hit) return hit;
const upstream = await fetch(
"https://api.example.org/v1/items?limit=12",
{
headers: {
"User-Agent": "example.org ([email protected])",
// "Authorization": `Bearer ${context.env.API_KEY}`,
},
}
);
if (!upstream.ok) {
return new Response(JSON.stringify({ error: "upstream unavailable" }), {
status: 502,
headers: { "content-type": "application/json" },
});
}
const response = new Response(await upstream.text(), {
headers: {
"content-type": "application/json",
"cache-control": "public, max-age=86400",
"access-control-allow-origin": "*",
},
});
context.waitUntil(cache.put(cacheKey, response.clone()));
return response;
}
Your page then fetches /api/feed, same origin, no key in the client, and the upstream API sees one request per day per edge location rather than one per visitor.
The Parts That Matter
The cache key is a URL, and it does not have to be real. https://cache.internal/feed/2026-09-21 is never requested by anyone. It exists so the Cache API has something to key on. Putting the date in it is what makes the content rotate: at midnight UTC the key changes, the lookup misses, and one visitor pays for a fresh fetch on behalf of everyone else that day.
response.clone() is not optional. A Response body is a stream that can be consumed once. Hand the original to cache.put and you return an already-drained body to the visitor.
context.waitUntil keeps the write alive. Without it the function may return before cache.put finishes, and the write gets cancelled. You would still serve correct responses and just never cache anything, which is a difficult bug to notice because everything works.
Secrets live in context.env. Set them in the Pages dashboard under environment variables, not in the source. This is the main reason to route a keyed API through a function rather than calling it from the page.
Rotation Granularity
Slice the ISO string to change the window:
slice(0, 10)gives2026-09-21, a daily key.slice(0, 7)gives2026-09, monthly.slice(0, 13)gives2026-09-21T14, hourly.
For anything finer than hourly, ask whether you need a cache at all or whether you should just proxy with a short cache-control.
When To Use This Instead of a Deploy Hook
The alternative to a function is a Cloudflare Cron Trigger that calls your Pages deploy hook nightly, which rebuilds the site and re-fetches at build time. That keeps the page fully static, which is faster and simpler to reason about.
Prefer the rebuild when the data is small and the site builds quickly. Prefer the function when the build is slow, when the data is large enough that baking it into every page is wasteful, or when you need a key kept off the client.
The two compose fine. Static content from the build, one function for the part that has to move.