All posts

Beyond Forecasts: Aggregating Government Weather Alerts for Outdoor Safety

Published Oct 18, 20244 min read
  • MeteoTrail
  • Engineering
  • Mountain

Key takeaways

  • Meteoalarm's Atom feed and each country's CAP format are awkward to consume directly, so we normalize them once for a fixed regional watchlist instead of leaving every product to parse them itself.
  • The service fails closed on missing API keys (401 by default) and distinguishes partial failures (200 with an errors array) from total upstream outages (503).
  • Unlike MeteoTrail, this is internal, API-key-only infrastructure with no public UI, and downstream products inherit its failures silently.

After we shipped MeteoTrail — our checkpoint-level forecasting product for outdoor sports events — most of our attention went into model accuracy: wind, precipitation, and temperature at a specific point on a mountain, at a specific hour. What I didn't fully appreciate until Val d'Aran by UTMB is how much organizational weight a piece of software ends up carrying once race directors are staring at it to make go/no-go calls. That experience changed how I think about anything we build that touches safety decisions, even indirectly.

In the months after, a smaller, unrelated-looking gap kept resurfacing. Official government weather alerts — the kind that say "orange wind warning for this region starting tomorrow at 06:00" — are public and genuinely useful, but nobody had built anything for us to consume them programmatically. Every product that cared had to go find them itself. This post is about the small internal service we built to stop that from happening.

1. Public data, badly shaped for consumption

Europe already solved the discovery problem for weather alerts, sort of. Meteoalarm aggregates official alerts from every national meteorological agency on the continent and republishes them as an Atom feed, one feed per country. That's genuinely useful — you don't have to know which of thirty-odd agencies to poll. But the feed itself is a summary: title, a rough region code, a severity color, a link out.

The actual content of an alert — what phenomenon, what thresholds, what time window, in what precise area — lives in a CAP (Common Alerting Protocol) payload, published by each country's own agency in its own dialect of the standard. For Spain, that's AEMET. "Standard" in the alerting world still leaves a lot of room for per-agency quirks in area codes, severity vocabularies, and XML structure.

Consuming this properly means two fetches, two formats, and reconciling both into something a product can branch logic on. Fine to do once. Not fine to ask every internal team to do for itself.

2. What we actually built

We built the Spain Meteo Alerts Aggregator: a small FastAPI service with one job — fetch, filter, normalize, and cache. It doesn't try to cover all of Spain. It works off a fixed watchlist of regions relevant to our outdoor and mountain-tourism products: Lleida, Aran (Val d'Aran), and several Mallorca coastal zones — south, tramontana, interior, levante, and north-northeast.

The pipeline per request is straightforward:

  1. Fetch the Spain Meteoalarm Atom feed.
  2. Filter entries down to the watchlist regions.
  3. For each matching alert, fetch the AEMET CAP payload with the actual detail.
  4. Normalize everything — Meteoalarm's feed shape and AEMET's CAP shape — into one stable JSON schema.
  5. Group the result by region, then by severity (red, orange, yellow, unknown).
  6. Drop any region from the response that currently has no active alert, so callers don't have to filter empty noise themselves.

An illustrative, simplified shape of what GET /alerts returns:

{
  "generated_at": "2024-10-18T07:15:00Z",
  "regions": {
    "aran": {
      "red": [],
      "orange": [
        {
          "id": "aemet-es-es610-20241018-0001",
          "phenomenon": "wind",
          "onset": "2024-10-19T06:00:00Z",
          "expires": "2024-10-19T18:00:00Z",
          "headline": "Orange wind warning"
        }
      ],
      "yellow": [],
      "unknown": []
    },
    "mallorca-tramontana": {
      "red": [],
      "orange": [],
      "yellow": [
        { "id": "aemet-es-illesbalears-20241018-0007", "phenomenon": "rain" }
      ],
      "unknown": []
    }
  },
  "errors": []
}

Nothing exotic. The value isn't in any single step — it's in having done the reconciliation once, in one place, instead of six times across six products.

3. Failing closed, and failing partially

Two design choices here came directly from lessons I didn't want to relearn.

First: authentication. /alerts, /areas, and /areas/{country} all require an X-API-Key header. /health and / stay open, because uptime monitors need somewhere to poke without credentials. What I care about is what happens when the service has no API keys configured at all — a misconfigured deploy, a missing secret, whatever. It rejects every protected request with 401. It does not fall back to open access. A service with no keys configured is a service that forgot to configure keys, not a service that intends to be public.

Second, and more subtle: GET /alerts distinguishes two very different failure modes. If the upstream Meteoalarm feed fetch fails entirely, or the service can't build a summary it would trust, it returns 503 — "I have nothing reliable to tell you, don't act on this response." But if the feed is fine and most regions resolved correctly while, say, one AEMET CAP lookup times out, it returns 200 with the successful regions populated and a non-empty errors array describing what didn't resolve — "here's what I know, and here's what I don't, you decide what to do with the gap." Downstream products can react very differently to those two cases.

4. Why this isn't MeteoTrail

This service will never have a landing page. There's no UI, no public sign-up, no marketing copy — just an internal FastAPI backend that a handful of our own products call with a key. That's exactly why the fail-closed and partial-failure design matters more here than it might for something consumer-facing.

When a public product breaks, a user sees an error and reloads. When a piece of internal infrastructure that other services silently depend on breaks — or worse, degrades quietly and returns something that looks fine but isn't — every downstream product built on top of it inherits that failure without knowing it. That's a much less forgiving bar than "does the forecast page look nice." It's also, I think, the right bar for anything sitting quietly underneath decisions that touch someone's safety, even at one remove.

We didn't build this because it was hard or interesting. We built it because after Val d'Aran by UTMB, I'd rather spend an afternoon designing for the failure case up front than find out later, from a downstream product, that we hadn't.