Why Daily Aggregates Lie: How WeatherAPI Builds day.avgtemp_c and Why Your Business Logic Shouldn’t Trust It Blindly

The field looks simple. The construction isn’t.

Call the forecast endpoint and read day.avgtemp_c and the number feels authoritative. One temperature, for a whole day, clean and unambiguous. But it’s a derived aggregate, not a measurement — and the way it’s derived matters enormously for certain use cases, and barely at all for others. Knowing which camp your app lives in saves you from quietly wrong outputs.

Here’s the short version: day.avgtemp_c is the arithmetic mean of the 24 hourly temperature values for that forecast day. Not a weighted mean, not a mean of the daily max and min (though meteorologists have traditionally used that shortcut — it’s the method NWS still documents for its degree-day products), just a straight average across every hour in the UTC-local-aligned day window. day.maxtemp_c and day.mintemp_c are the highest and lowest of those same 24 values, respectively.

That sounds reasonable until you think about what “day” actually means here.

The Day Boundary Problem

The daily forecast window aligns to the requested location’s local time zone — midnight to midnight local. That matters because the coldest hour of the day is almost always just before sunrise, which might be 5am or 6am local depending on the season. For a location like Helsinki in January, the hourly slot at 5am on Day 2 sits comfortably inside the Day 2 window. But for locations where day-boundary math gets genuinely ambiguous — anywhere with half-hour or 45-minute UTC offsets like Kolkata or Kathmandu, or locations near the international date line — there’s real risk that the “coldest hour” falls in the adjacent day’s window instead.

The less-discussed version of this problem is DST transition. On a day when clocks spring forward, the local day has 23 hours. The daily aggregate becomes a mean of 23 values, not 24. Max and min are still correct — “what’s the highest value in this set” doesn’t care how many elements the set has — but avgtemp_c is technically the mean of 23 observations. This isn’t documented anywhere obvious. Most API consumers never notice. But if you’re running degree-day calculations for energy or agriculture across a DST transition date, you’re off by one hour’s worth of data in your average, once a year, at every affected location.

How This Breaks Degree-Day Calculations Specifically

Heating degree days (HDD) and cooling degree days (CDD) are a standard metric in energy demand forecasting and agricultural science. The classic formula uses the simple average of daily max and min:

HDD = max(0, base_temp - ((maxtemp + mintemp) / 2))

NWS defaults to a base of 65°F (18.3°C) for this. If you plug day.avgtemp_c into that formula instead of computing (maxtemp_c + mintemp_c) / 2 yourself, you’ll usually get a similar answer — but not always the same one, and the direction of the difference isn’t random.

On a day with a long warm afternoon and a brief cold spike at dawn, the hourly mean avgtemp_c skews warmer than (max + min) / 2, because 14 warm hours outweigh 1-2 cold hours arithmetically. That makes avgtemp_c understate HDD relative to the traditional formula. The gap is usually under 0.5°C on a typical mid-latitude day, but it’s systematic — it pushes in the same direction for a given climate pattern, it doesn’t average out over time.

Accumulate that across a full heating season (October through April in northern Europe) and a consistent 0.3°C bias works out to roughly 50–60 HDD units. Inside the noise for most applications. For a utility billing on degree-day-adjusted consumption, less so.

The cleanest fix: don’t use day.avgtemp_c for degree-day work. Pull the hourly array and integrate against your baseline directly:

// Pseudo-code — adapt for your stack
const hours = forecastDay.hour; // 24-element array
const hdd = hours.reduce((acc, h) => {
  return acc + Math.max(0, 18.3 - h.temp_c);
}, 0) / 24;

This gives you a true hourly-integrated degree-day value — strictly more accurate than either formula that operates only on daily max/min or daily average. It also handles the 23-hour DST edge case naturally, because you’re summing actual available slots rather than assuming 24.

What maxtemp_c and mintemp_c Are Actually Good For

Despite the above, the daily max and min fields are the most reliable aggregates in the daily object for a simple reason: they’re answering a monotone question. “What is the highest value in this array?” doesn’t depend on how many elements are in the array and doesn’t depend on how temperatures distribute across the day. A DST transition doesn’t change which hour hit the peak. These fields are robust.

For alerting use cases — frost warnings, heat advisories, anything threshold-based — use max and min directly. “Will it drop below 2°C today?” is answered cleanly by mintemp_c < 2. Don’t average your way into a false sense of safety.

The precip_mm Version of This Problem

day.totalprecip_mm has the same construct-vs-measure issue. It’s a sum of hourly precipitation values, not a separate model output — which means hourly rounding compounds. GRIB2 model output has finite precision, and we round to two decimal places in the API response. On a day with 24 light-drizzle hours each reading 0.01mm, the daily total reports 0.24mm. If the model’s actual value for each hour was 0.014mm, you’ve lost roughly 12% of the signal to rounding. That matters for millimeter-precision irrigation scheduling; it doesn’t matter for deciding whether to carry an umbrella.

Serving more significant figures across tens of thousands of locations per response has a real cost, and the use cases this would fix are narrow. But knowing the mechanism matters if you’re in one of them.

One Pattern That Helps Across All of This

If your application makes decisions that depend on fine-grained daily aggregates, treat the daily object as a convenience cache rather than the source of truth. Fetch the hourly array and build whatever aggregates your logic actually needs. The daily fields are fine for display — showing a user “High: 24°C, Low: 11°C” on a forecast card is exactly what they’re for. Computing business metrics from them means trusting construction assumptions that aren’t in the API spec, because most consumers never need to care about them.

The requests cost the same either way. A 3-day hourly array is not a meaningfully larger payload than the daily summary fields. You’re not saving latency or quota by relying on the aggregates.

Worth checking before you move on: how many places in your codebase are reading a day.* field and treating it as a direct model output rather than something computed over hourly slots?

Scroll to Top