If an upstream API is rate-limited, the fix is to stop hitting it per request: a cron val fetches the upstream every 15 minutes and writes the result to blob storage, and an HTTP val serves that cached blob instantly. Your endpoint never talks to the upstream on request, so it's fast and your traffic never counts against the upstream's rate limit.
Why a keyed upstream matters
Section titled “Why a keyed upstream matters”On Val Town — or any serverless platform — outbound requests leave from shared egress IPs. Keyless APIs that rate-limit by IP (Open-Meteo, for example) count every other user's requests on that IP against "your" limit, so even one polite cron call can come back 429. An earlier version of this template used Open-Meteo and hit exactly that failure: the refresh cron itself got rate-limited by other users' traffic.
The fix is an upstream with a per-key quota. This template uses OpenWeatherMap: the quota belongs to your API key, so nobody else on the shared IP can drain it. The cron-to-blob cache then keeps your own usage to one upstream call per interval, no matter how much traffic your endpoint gets.
The two files
Section titled “The two files”The cron half fetches the upstream and writes { fetchedAt, data } to a
blob:
View and run this example on Val Town
import { blob } from "https://esm.town/v/std/blob/main.ts";
// Upstream: OpenWeatherMap current weather. It is keyed, so the quota belongs// to your API key -- not to Val Town's shared egress IPs. Keyless IP-rate-limited// APIs (like Open-Meteo) get drained by other users on the same shared IPs;// a keyed upstream plus this cron-to-blob cache avoids that.export default async function () { const key = Deno.env.get("OPENWEATHER_API_KEY"); if (!key) { throw new Error( "Missing OPENWEATHER_API_KEY env var -- get a free key at https://openweathermap.org/api and add it in this val's Environment Variables", ); }
const upstream = `https://api.openweathermap.org/data/2.5/weather?q=Brooklyn,US&units=imperial&appid=${key}`; const res = await fetch(upstream); if (!res.ok) throw new Error(`Upstream ${res.status}: ${await res.text()}`); const data = await res.json(); await blob.setJSON("cache", { fetchedAt: new Date().toISOString(), data }); console.log("Cache refreshed at", new Date().toISOString());}The HTTP half serves the cache with an X-Cache-Age header (and a 503 if
the cache is empty):
View and run this example on Val Town
import { blob } from "https://esm.town/v/std/blob/main.ts";
// Serves the cached upstream response instantly from blob storage.// refresh.ts (cron) is the only thing that talks to the upstream API.export default async function (): Promise<Response> { const cached = await blob.getJSON("cache") as | { fetchedAt: string; data: unknown } | undefined;
if (!cached) { return Response.json( { error: "Cache is empty. Run refresh.ts once or wait for the cron." }, { status: 503 }, ); }
const ageSeconds = Math.round( (Date.now() - new Date(cached.fetchedAt).getTime()) / 1000, );
return Response.json(cached.data, { headers: { "X-Cache-Age": String(ageSeconds), "X-Cache-Fetched-At": cached.fetchedAt, }, });}Set it up
Section titled “Set it up”- Remix this template — click the Remix button in the top right corner.
- Get a free API key at openweathermap.org/api.
- Add it to your val's
environment variables as
OPENWEATHER_API_KEY. - Click Run on
refresh.tsonce to populate the cache, or wait for the first cron run. - Hit the
main.tsendpoint URL and check theX-Cache-Ageresponse header to see how fresh the data is.
To adapt it to your own API, change the URL in refresh.ts and adjust the
cron schedule to taste. Keep reading the key with Deno.env.get() — it
stays server-side and is never exposed to your endpoint's callers.
- The 15-minute schedule (
*/15 * * * *) is the fastest a cron can run on the free plan; Pro allows up to once a minute. - Cached data survives between runs in blob storage, so a failed refresh just means slightly staler data, not an outage.
Next steps
Section titled “Next steps”- Blob storage reference — what the cache is built on
- Cron reference — schedule types and limits
- Environment variables — how secrets stay private