The Number Everyone Misuses
chance_of_rain looks simple. It’s a percentage. Threshold it at 50%, fire an alert, done. If you’ve built this and actually run it in production, you already know what happens: alerts firing for a 20-minute drizzle at 3am, repeated pings for the same front as it moves through hourly buckets. The percentage alone doesn’t carry enough signal. You need at least two other fields, and you need to think about how your logic accumulates across hours, not just within a single one.
What chance_of_rain Actually Represents
The field is a probability of measurable precipitation occurring within that hour, derived from ensemble model output. “Measurable” in WMO terms typically means ≥0.2mm. That threshold matters: a 70% chance_of_rain could mean a very high probability of a trace amount, or a moderate probability of a heavy shower. The percentage doesn’t encode intensity. It just tells you how likely something is to fall.
This is why you can see chance_of_rain: 80 paired with precip_mm: 0.3 — high confidence, trivial amount. For most practical alert use cases (warning a user to bring an umbrella, or a site manager to cover equipment), intensity matters as much as probability. Often more.
The Three Fields You Actually Need
From the WeatherAPI forecast endpoint’s hourly block, the combination that holds up in practice:
- chance_of_rain — probability gate
- precip_mm — intensity filter
- will_it_rain — binary confirmation that model output has crossed an internal threshold for a deterministic yes/no
will_it_rain is 0 or 1, and it’s not simply chance_of_rain >= 50. It incorporates additional model confidence signals, which is why it’s worth using as a second gate rather than dismissing it as redundant with the percentage.
A basic three-field gate:
bool ShouldAlert(HourlyForecast h) =>
h.ChanceOfRain >= 60 &&
h.WillItRain == 1 &&
h.PrecipMm >= 1.0;
That alone cuts a significant share of false positives. But it still fires for isolated single-hour events, which is the harder problem.
Duration Windows: Don’t Alert on a Single Hour
A single hourly bucket satisfying your threshold is often noise — a convective cell that may or may not materialize, or a model artifact at the edge of a front. For outdoor work alerts, delivery routing, or sports scheduling, you almost always care about sustained precipitation: two or more hours above your thresholds, or a cumulative amount within a window.
A sliding-window check over a 3-hour lookahead, which tends to be the sweet spot for actionable alerts without over-predicting:
bool HasSustainedRain(List<HourlyForecast> hours, int startIndex, int windowSize = 3)
{
int qualifyingHours = 0;
double cumulativeMm = 0;
for (int i = startIndex; i < startIndex + windowSize && i < hours.Count; i++)
{
var h = hours[i];
if (h.ChanceOfRain >= 60 && h.WillItRain == 1 && h.PrecipMm >= 0.8)
{
qualifyingHours++;
cumulativeMm += h.PrecipMm;
}
}
// Require at least 2 qualifying hours OR cumulative amount above 3mm
return qualifyingHours >= 2 || cumulativeMm >= 3.0;
}
The cumulative branch catches intense short bursts — 5mm in one hour is worth alerting on even if adjacent hours are dry. The qualifying-hours branch catches persistent light rain that individually wouldn’t cross an intensity threshold but adds up to a genuinely disruptive day.
Timing Relevance: Suppress Alerts for Inconvenient Hours
An alert at 2am about 6am rain is probably fine. An alert at 11pm about rain at midnight probably isn’t actionable. This is application-specific, but worth building in from the start:
bool IsActionableHour(DateTime forecastTime, int earliestHour = 6, int latestHour = 22)
{
int hour = forecastTime.Hour;
return hour >= earliestHour && hour <= latestHour;
}
Combine this with your window check so alerts only surface when qualifying hours overlap with your active window. For construction or field operations this matters practically — a superintendent needs to know about morning rain before crew dispatch, not at midnight when there’s nothing to be done about it.
Deduplication Across Poll Cycles
If you’re polling the forecast endpoint every 30 minutes, your alert logic will evaluate the same upcoming rain event multiple times. Without deduplication, you’ll fire the same “rain expected at 14:00” alert several times as the event stays above threshold on successive polls.
The cleanest approach is to key your alert state on a combination of location ID and the first qualifying hour’s epoch timestamp, stored in Redis with a TTL slightly longer than your suppression window:
string alertKey = $"rain_alert:{locationId}:{firstQualifyingEpoch}";
bool alreadyFired = await redis.KeyExistsAsync(alertKey);
if (!alreadyFired)
{
await FireAlert(locationId, forecastSummary);
await redis.StringSetAsync(alertKey, "1", TimeSpan.FromHours(4));
}
Four hours covers most frontal systems without suppressing genuinely new events later in the day. In a maritime climate (Glasgow is a good test case) you’ll want a longer window than somewhere convective cells clear fast — the same alert logic running in Phoenix and Seattle needs different TTL tuning.
One Caveat Worth Naming
The precip_mm field in the hourly forecast is a model-derived estimate, not an observation. In the near term (hours 1-6), the HRRR model at 3km resolution is giving you its best guess at convective initiation, which can flip from 0mm to 4mm between runs. Beyond hour 12 you’re looking at GFS or NAM output at coarser resolution, where individual shower cells aren’t resolved — you’re getting a probability smear across a grid cell that might be 12km across. That’s not a reason to stop using the field, but it’s a reason not to build hard operational decisions (canceling an outdoor event, dispatching equipment) purely off the 48-hour outlook without re-checking as you get closer.
The alert logic above works well inside a 6-12 hour window. Beyond that, treat it as an early flag that needs confirmation before anyone acts on it.
Putting It Together
The full evaluation loop for a single location, run on each poll cycle, ends up being roughly 40-50 lines: fetch the hourly array, slide the window across the next 12-24 hours, check thresholds, apply the timing filter, dedup against Redis, fire. No complex rules engine required — just resistance to the urge to treat a single field as sufficient.
For multiple locations — fleet tracking, field sensor networks, multi-site construction — the same logic runs per location. Batch your forecast requests to stay within rate limits and cache aggressively at the location level. Re-fetching on every poll cycle for 50+ locations adds up fast and doesn’t buy you much given how infrequently model output actually changes between runs.
Before you ship: what does a false negative cost versus a false positive in your specific use case? For consumer apps, a missed light-drizzle alert is usually fine — crying wolf is worse. For operational use, invert that and tune thresholds lower, then let the deduplication layer manage volume. Getting that call wrong at the start is what leads to the alert logic getting ripped out six months later.
