Cold Weather, Warm Cache: How to Set TTLs That Actually Match Forecast Update Cycles

The TTL problem nobody talks about

Most developers caching weather API responses pick a TTL by feel — 5 minutes, 15 minutes, an hour. Most of the time it works. But the moment someone complains about stale forecast data, or you realize you’re burning quota re-fetching data that hasn’t changed yet, the question becomes: how long is a weather forecast actually valid?

That depends entirely on which model produced it and what kind of data you’re caching. Getting this right isn’t complicated, but it does require knowing a little about how the models actually run.

Model runs are not continuous — they’re scheduled

HRRR, NAM, GFS, and ECMWF don’t update constantly. They run on fixed schedules, and each run takes time to complete before output is available for ingestion. Those schedules are the key to setting TTLs that aren’t completely arbitrary.

HRRR

HRRR runs every hour, initializing at the top of each UTC hour. Output for a given run typically becomes available within 30–45 minutes of initialization. So if you’re caching an hourly forecast produced from the 14Z HRRR run, that data is current until roughly 15Z, when the next run’s output arrives. Caching HRRR-backed hourly data for more than an hour means you’re almost certainly serving output from a stale run when a fresher one exists.

The flip side: caching it for 45 minutes instead of 10 doesn’t hurt you at all. The underlying model hasn’t changed. Aggressive short TTLs on HRRR data mostly just waste quota.

NAM

NAM runs four times daily: 00Z, 06Z, 12Z, 18Z. Each run covers out to 84 hours. Between those runs, there’s no new NAM data — so if you cached a NAM-backed 3-day forecast at 13Z, that data is valid until the 18Z run completes, roughly 19Z–20Z. That’s a 6-hour cache window you could defensibly hold, not 15 minutes.

GFS

GFS also runs four times daily (00Z, 06Z, 12Z, 18Z) but covers out to 384 hours. For anything beyond day 3 in a forecast response, GFS is almost certainly the source. A 10-day forecast cached at noon doesn’t meaningfully improve if you refresh it every 30 minutes — the model won’t run again until 18Z at the earliest, and GFS output at that range diverges slowly. Caching 10-day data for 3–4 hours is reasonable and probably more honest about how the data actually ages.

ECMWF

ECMWF runs twice daily (00Z and 12Z), and output takes longer to arrive — often 5–6 hours after initialization. Its medium-range output (days 5–10) doesn’t meaningfully change between runs in most cases. Daily TTLs aren’t unreasonable for the tail end of a 10-day forecast backed by ECMWF.

Translating this into a layered TTL strategy

Rather than one TTL for all weather data, split your cache by forecast horizon:

  • Current conditions / real-time: 5–10 minutes. METAR observations update on their own schedule (typically every 30–60 minutes at most stations, more frequently at major airports), but API-side aggregation can lag. Refreshing more often than every 5 minutes rarely gets you newer underlying data.
  • Hourly forecast, hours 1–24: 45–60 minutes. Aligned with HRRR’s hourly run cycle. If HRRR output for your region isn’t available (outside CONUS, for instance), NAM takes over and 90 minutes is defensible.
  • Hourly forecast, hours 24–48: 90–120 minutes. NAM territory. Refresh roughly when the next NAM run completes.
  • Daily forecast, days 3–7: 3–4 hours. GFS runs four times daily. Refreshing more often is redundant.
  • Daily forecast, days 8–14: 6–12 hours. GFS and ECMWF at this range are ensemble output — the signal changes slowly. There’s no operational reason to cache-bust this every 30 minutes.

None of these numbers are exact. There’s propagation delay between when a model run completes and when output reaches any downstream API. But they’re much closer to reality than a flat TTL applied to every forecast horizon equally.

Where this goes wrong in practice

The two failure modes we see most often are opposite ends of the same mistake.

The first is aggressive over-fetching: a developer sets a 60-second TTL on a 7-day forecast, burns through quota, and wonders why they’re hitting rate limits. The data at second 60 is identical to second 0 — the models haven’t run again. They’re re-requesting the same computation.

The second is under-fetching on current conditions: someone caches real-time data for 4 hours to save quota, then builds a weather dashboard that confidently displays yesterday afternoon’s temperature at 2am. That’s a product-level trust problem, not just a technical inefficiency.

Both are solved by the same thing: treating forecast horizon as the primary variable in your TTL logic.

A concrete implementation pattern

In practice this means your cache key should encode the horizon bucket, not just the location. Something like:

weather:current:{location_hash}      → TTL 300s
weather:hourly_short:{location_hash} → TTL 3600s
weather:hourly_mid:{location_hash}   → TTL 5400s
weather:daily_near:{location_hash}   → TTL 10800s
weather:daily_far:{location_hash}    → TTL 28800s

You make one API call that returns the full forecast, then write slices of that response into separate cache keys with different TTLs. Your application layer reads from whichever bucket it needs. Current conditions can go stale and get refreshed without invalidating the 10-day forecast you fetched an hour ago — which is still perfectly current.

Redis or KeyDB both handle this trivially with per-key EXPIRE. No complicated invalidation logic required.

One caveat worth flagging

Model run schedules can slip. NOAA occasionally delays GFS or NAM output due to compute issues, and HRRR availability can vary by region depending on how long the assimilation step takes. If you build hard cache expiry logic around exact UTC model run times, you’ll occasionally serve slightly stale data while waiting for output that’s running late. Building in a small overlap — expiring your cache 15–20 minutes after the theoretical next-run availability rather than exactly on it — handles most of this without adding meaningful staleness.

This also only matters if you have real traffic or meaningful quota constraints. If you’re building a low-volume internal tool making a few dozen requests a day, the model run schedule is trivia. The layered approach earns its complexity at scale.

The real question to sit with

Before setting any TTL, ask: what’s the fastest the data behind this response could actually change? For most forecast horizons, the answer is much slower than developers assume — and your cache should reflect that, not the cadence you’d pick if quota were free.

Scroll to Top