The instinct makes sense: each customer gets their own API key, quota is isolated, billing is simple to attribute. For a lot of SaaS infrastructure this pattern works fine. For weather API calls it’s quietly expensive and structurally wrong — and it compounds in ways that only show up once you’re past a few dozen tenants.
Here’s the actual problem. Weather data for a given location at a given time is the same for every tenant who asks for it. If tenants A, B, and C all serve users in London, and your app makes an API call per tenant rather than per location, you’re making three network calls, hitting quota three times, and caching nothing useful — because each key has its own response cache, or more likely no shared cache at all. The data returned is identical. The cost is not.
What Collapses When You Scale This
A shared-key, shared-cache architecture lets you serve tens of thousands of location requests per day from a fraction of the underlying API calls, because most tenants’ users cluster around the same cities. Per-tenant keys kill that entirely. Each tenant’s cache is isolated and cold, so every cache miss hits the upstream API — and with separate keys you can’t deduplicate across them even when the request is byte-for-byte identical.
Quota exhaustion also becomes unpredictable. A tenant on a modest plan spikes requests — a marketing campaign goes out, traffic floods in — and they burn through quota for data that’s already sitting in another tenant’s cache. You have no way to serve it from there because the keys are separate. You either call upstream again (wasting quota) or return an error (bad for the user).
Rate limit handling gets messy fast. Per-tenant keys mean per-tenant retry logic, per-tenant backoff, per-tenant quota monitoring. With a shared key you write that logic once and it covers every tenant’s requests through the same pipeline.
The Correct Shape of the Architecture
Your backend should own exactly one WeatherAPI key (or a small pool if you’re at genuine enterprise scale — more on that below). All tenant requests route through your backend, which functions as a thin caching and attribution proxy. The flow:
- Tenant request arrives with a location (lat/lon, or a location string — see below).
- Your backend normalizes the location to a canonical cache key.
- Check Redis/KeyDB: cache hit with a valid TTL, return the response. Done.
- Cache miss: call WeatherAPI with your shared key, store the response, return it.
- Log the request against the tenant for attribution — usage, billing, analytics, whatever you need.
One API key. One cache. All tenants benefit from every other tenant’s prior requests. A request for London at 14:00 UTC made by Tenant A makes the response free for Tenants B through Z for the duration of the TTL.
Location Normalization Is the Hard Part
The cache key has to be stable. If Tenant A sends "London,UK" and Tenant B sends "51.5074,-0.1278" and Tenant C sends "London", you have three different cache keys for the same location — three upstream calls, zero cache reuse.
The fix is to resolve every location to a canonical lat/lon before it touches your cache key logic. Round to three or four decimal places — enough precision to distinguish adjacent neighborhoods, not so precise that every request misses. At three decimal places, coordinates snap to roughly a 100-meter grid. That’s finer than any NWP grid you’re actually querying (HRRR runs at 3km, GFS at 25km), so you’re not losing meaningful spatial resolution, but you are collapsing the long tail of microscopically-different coordinates into shared cache entries.
A string like "London,UK" needs to resolve to a lat/lon first — either via WeatherAPI’s geocoding endpoint (the response includes lat and lon you can store), or a local city lookup table if you want to avoid that round trip. Once resolved, normalize to lat_3dp:lon_3dp and use that as your cache key. Every tenant querying the same city now shares the same cache entry.
This is the step teams skip. It’s also exactly what determines whether your cache is effective or just a thin layer that misses constantly.
TTLs Should Match Forecast Update Cycles, Not a Round Number
A 5-minute TTL sounds responsive. But current conditions from WeatherAPI are backed by METAR observations, which typically update every 30 minutes per ICAO standards. Refreshing the cache every 5 minutes means roughly 5 upstream calls for every 1 that returns new data. A TTL of 20 to 25 minutes for current conditions is more honest about what’s actually changing.
For hourly and daily forecasts, the underlying NWP models update on their own schedules — HRRR hourly, GFS every 6 hours. A 1-hour TTL for hourly forecast data is generally safe: you won’t miss a model update cycle and you’ll serve far fewer upstream calls. The principle is to match the TTL to when the data actually changes, not to what feels fast.
Attribution Without Per-Key Isolation
The reason some teams reach for per-tenant keys is attribution — they want to know which tenant consumed how much quota. Legitimate need, wrong solution. Log the tenant ID, the canonical location key, and the cache hit/miss status on every request through your proxy layer. Now you know exactly how many upstream calls each tenant triggered, which is different from how many requests they made — and arguably more useful for billing.
A tenant who makes 10,000 requests against cached data costs you essentially nothing in upstream quota. A tenant who makes 200 requests but always for novel locations costs you 200 API calls. Per-request billing doesn’t capture that difference; per-upstream-call or per-location-per-period billing does. You can only implement either correctly if your proxy layer is tracking what actually went upstream.
When a Key Pool Actually Makes Sense
If you’re on an enterprise plan and your request volume is high enough that a single key’s rate limits are a real ceiling, a key pool is reasonable. But that’s a throughput problem, not a multi-tenancy problem. You still share one cache layer. Requests round-robin across keys based on current rate-limit headroom, not based on which tenant is asking.
The failure mode to avoid: assigning each key in the pool to a tenant. That just recreates the original problem at slightly higher scale.
A Concrete Starting Point
We use KeyDB in our own infrastructure, mostly for the better write throughput under concurrent load. The cache logic itself is maybe 40 lines: resolve location, build cache key, GET, hit or miss, on miss call WeatherAPI, SETEX with your TTL, log the upstream call against the tenant, return. The normalization step adds a rounding function or a geocoding lookup. None of it is complicated to build.
Before you wire up the next tenant with a fresh API key: how many of your tenants are actually serving different locations, versus different end users in the same cities? If it’s the latter, you’re paying for that overlap on every single request.
