# GEFS ensemble exceedance: how many members cross your threshold? > Averaging 31 GEFS members destroys a threshold signal weeks ahead. One paid call counts them member by member and returns a counted fraction per day, with the member count behind it. Published 2026-08-10 · Updated 2026-08-16 · HTML version: https://invoket.com/blog/how-many-ensemble-members-cross-your-threshold --- An agent that has to commit weeks ahead — a parametric payout trigger, a shipping window, a harvest slot, a concrete pour — asks a threshold question: *will this day see at least 30 mm of rain?* Past roughly two weeks, the usual answer is an ensemble **mean**, and averaging is exactly the operation that destroys a threshold signal. One paid call — [`GET /weather/forecast/exceedance`](/docs/api/weather-forecast-exceedance) — counts the NOAA GEFS members **one by one, before any averaging**, and returns one row per UTC calendar day: members crossing, members counted, and the fraction of the two. The whole series settles as one x402 payment. The number it returns is a **counted fraction, not a calibrated probability**. That distinction is the substance of this article, and the endpoint states it in its own response. ## The problem: an average cannot answer a threshold question Past the deterministic window (about 384 h, or J+16), [`GET /weather/forecast`](/docs/api/weather-forecast) and [`GET /weather/forecast/daily`](/docs/api/weather-forecast-daily) serve the average of ~31 ensemble members. That is what an ensemble mean *is*, not a defect: ECMWF's Forecast User Guide states plainly that the mean "smooths the flow more in areas of large uncertainty (large spread)" and that with large spread it "can be a rather weak pattern and may not represent any of the possible states" ([ECMWF FUG §8.1.2](https://confluence.ecmwf.int/display/FUG/Section+8.1.2+ENS+Mean+and+Spread)). An extreme produced by a subset of members survives the average as a fraction of itself. Measured in production on 2026-07-25 for Paris, on `/weather/forecast/daily?lat=48.85&lon=2.35&days=35`: | Segment | Register | Daily precipitation | Daily max temperature | |---------|----------|---------------------|-----------------------| | J+0 → J+15 (16 days) | deterministic GFS | 9 days at `0.0` mm, peak **`13.12`** mm | 21.2 → **40.5 °C**, a 19.3 °C spread | | J+16 → J+34 (19 days) | GEFS ensemble mean | **no** day below `0.40` mm, none above **`1.82`** mm | 25.3 → **27.9 °C**, a 2.6 °C spread | Read the second row as an agent would: a rule like *"more than 30 mm in a day"* placed on that tail will **essentially never fire**, whatever weather actually occurs. Nineteen days of a 35-day series collapse into a band 1.4 mm wide. An agent polling the mean and comparing it to a contractual threshold is not getting a cautious answer — it is getting a structurally dead one. ## What counting the members instead looks like The endpoint reads the **member grids** rather than their mean, reduces each member to one value per day, and compares that value to your threshold. Three parameters carry the question: | Parameter | Role | |-----------|------| | `variable` | The quantity tested: `precipitation` (mm), `wind` (m/s) or `temperature` (°C) | | `threshold` | The threshold value, in that variable's unit — echoed back as `threshold_unit` | | `comparison` | `at_least` (default) or `at_most`; both bounds **inclusive** | `variable` and `threshold` are required *together*: a threshold without a quantity has no unit, and the unit applied is never implicit. The point is located either by coordinates or by place name (`city=Manila`, narrowed by `country=`), the same gazetteer contract as the other forecast routes. ```http GET /weather/forecast/exceedance?city=Manila&variable=precipitation&threshold=30&days=3 ``` ```json { "date": "2026-08-25", "statistic": "daily_sum", "members_total": 31, "members_exceeding": 9, "probability": 0.2903, "steps_used": 4, "coverage": { "complete": true } } ``` Captured in production on 2026-08-09. Read that row as *"9 of the 31 members put at least 30 mm of rain on 2026-08-25"*. Not *"a 29 % chance of rain"*. ## The honesty line: a counted fraction is not a probability This is the part an agent must not paper over. `0.2903` is `members_exceeding / members_total` and nothing else. No model output statistics, no bias correction against observations, no reliability calibration has been applied. Turning a raw ensemble frequency into a calibrated probability is a whole discipline — the statistical literature on tail calibration exists precisely because raw ensemble frequencies are poorly behaved at high thresholds ([Allen et al., *Tail calibration of probabilistic forecasts*](https://arxiv.org/html/2407.03167v3)). This endpoint does not pretend to have done that work. Two design consequences follow, and both are worth copying in any system that serves derived numbers: - **`members_total` is served on every single day**, never assumed equal to the nominal 31. The fraction can be divided back out and audited. A `0.5` counted on 4 members and a `0.5` counted on 31 are not the same statement, and the response hands you both numbers so you can tell them apart. - **The caveat travels with the number.** Every response carries a `method` object with `kind: "counted_member_fraction"`, `calibrated: false` and a note saying so in words. An agent that never reads a documentation page still receives the caveat attached to the value it is about to act on. `probability` is `null`, not `0`, when no member carried a value for that day — an empty sample has no fraction, and none is invented. ## The reduction is named, and the direction flips it Each member is reduced to one value per day *before* the threshold test, and the response names which reduction was applied in `statistic`: | `variable` | `comparison` | `statistic` | Reduction | |------------|--------------|-------------|-----------| | `precipitation` | either | `daily_sum` | Sum over the day, rebuilt per member from disjoint accumulation buckets | | `wind` | either | `daily_max` | Largest 10 m **scalar** speed `sqrt(u² + v²)`, per member — never a vector mean of components | | `temperature` | `at_least` | `daily_max` | Warmest value of the day | | `temperature` | `at_most` | `daily_min` | Coldest value of the day | That last row is the one that saves a real trigger. A frost threshold (`variable=temperature&threshold=0&comparison=at_most`) tested against the daily *maximum* would never fire; the direction of the comparison flips the reduction to `daily_min`. And a `0.0` on such a query is a **counted** zero on 31 members — an honest answer, not a missing one. ## Degraded days are served and marked, never dropped Ensemble tails are ragged at the edges. Four failure modes can appear in a day's `coverage.reason`, and none of them removes the day from the series: - **Clipped by the window start or end** — the first day of the tail usually begins mid-day; the day is served with the instant it is covered from. - **Below the member floor** — `counted on N of 31 ensemble members`. Under the 21-member floor the day is *still* served with its real `members_total`, and nothing is extrapolated back up to 31. - **Members with missing steps** — such a member is **counted, not dropped**. Dropping it would silently shrink the denominator and inflate the fraction of the members that remain. Days that fall outside the covered window are simply omitted, never an error. Only a window lying entirely outside the ensemble tail is a `400 OUT_OF_RANGE` — and that error names the deterministic routes to use instead, so an agent that aimed at the wrong horizon finds its way back without a human. Per the x402 golden rule, a well-formed request returning at least one counted day is a successful answer — **200** — even when an edge day is clipped: that is reported through `coverage`, never hidden. And when a deployment holds no member grids at all, the response is a 200 with an empty series flagged as carrying no usable result, which the gateway reads as a signal **not to settle**: the agent asked nothing wrong and is not charged for an empty answer. ## Scope, stated plainly - **It is a grid cell, not a weather station.** The member grids are a ~0.5° mesh; `grid.distance_km` exposes how far the served cell sits from your point. - **It is the tail, not the near term.** The window is capped at 20 days, the physical span of the tail the members cover. Inside the deterministic window, this is the wrong route and it says so. - **The member layer runs one cycle behind.** GEFS runs 4 cycles a day out to 16 days, *except* 35 days at 00 UTC — that extension publishes the following day, so a healthy response here is normally `cached`, not `live` ([NOAA EMC, GEFS v12](https://emc.ncep.noaa.gov/emc/pages/numerical_forecast_systems/gefs.php/)). - **Omit `days` and the default window is budget-capped**, flagged with `window_capped: true`; ask `days=` explicitly and it is served in full. ## Where it fits in the x402 loop The call follows the same pattern as every Invoket endpoint: discover, take the `402`, pay, replay. The [Quickstart](/docs/quickstart) walks that cycle with runnable snippets and [For agents](/docs/for-agents) covers the discovery surfaces. The whole day series — however many days it counts — settles as **one payment for the call**. The price is served live by the [catalog](https://api.invoket.com/catalog) and is not pinned in this article. Among the forecast routes, pick by the shape of the question: - [`GET /weather/forecast`](/docs/api/weather-forecast) — one lead time at one point, deterministic within 16 days. - [`GET /weather/forecast/daily`](/docs/api/weather-forecast-daily) — the daily series over the same range, in [one call](/blog/a-day-by-day-forecast-series-in-one-settlement). - [`GET /weather/forecast/exceedance`](/docs/api/weather-forecast-exceedance) — a counted threshold fraction over the ensemble tail, weeks out. - [`GET /climate/indices`](/docs/api/climate-indices) — the same threshold grammar applied to the *past*, on ERA5, for [parametric triggers](/blog/parametric-insurance-triggers-for-any-location) that need a measured index rather than a forecast one. A trigger agent usually needs two of these: the historical index that defines the contract, and the counted fraction that says how close the ensemble is to crossing it. Used for what it is — a raw counted member fraction, with the denominator exposed and the caveat attached — the exceedance route gives an agent something it can act on weeks ahead without inventing a probability nobody computed. For the full parameter list, coverage rules and error codes, see the [`GET /weather/forecast/exceedance` documentation](/docs/api/weather-forecast-exceedance). GEFS data comes from NOAA and is public-domain U.S. government data.