Why WeatherAPI stopped working for this use case

WeatherAPI is a fine general-purpose weather API. I picked it for the first version because it had a generous free tier, reliable uptime, and a clean JSON response shape. But I ran into two problems specific to marine use.

First, the wave data it provides comes from a third-party model that WeatherAPI resells. The resolution for offshore points is coarse — roughly 50km grid spacing — which is fine for broad swell conditions but misses the nearshore effects that actually determine what you'll experience at a specific beach. Swell refraction, bottom topography, point breaks: none of these show up in 50km grid output.

Second, and more practically: WeatherAPI changed its pricing structure in September 2025. The marine wave variables I was relying on moved to a paid tier. For a free, open-source tool I couldn't justify telling users to sign up for a paid API plan just to get swell data.

Why Open-Meteo

Open-Meteo stood out for a few reasons. It's genuinely free for non-commercial use with no API key for requests under 10,000/day — which is more than enough for a personal CLI. It sources its marine data from the ECMWF ERA5 reanalysis and the GFS wave model, both of which have good global coverage at 0.25° resolution.

The 0.25° resolution (roughly 25km) is still coarse for surf forecasting at a specific point, but it's better than what I had. And for the use case Zeemist targets — "is this morning broadly suitable for being on the water" — it's sufficient.

What broke

The migration took longer than I expected. Three things broke in non-obvious ways.

Unit inconsistency in swell period

WeatherAPI returns swell period in seconds. Open-Meteo returns it in seconds too, but the field name changed and the semantics differ slightly: WeatherAPI reports peak period (Tp), while Open-Meteo's wave_period is mean period (T02). Mean period is typically 20–30% shorter than peak period for mixed swell conditions. My display thresholds for what counted as a "good" swell window were calibrated against peak period, so after the migration everything looked worse than it was.

I fixed this by adding a conversion factor (multiply T02 by 1.25 as an approximation of Tp) and re-calibrating the thresholds. The factor is configurable in config.toml for users who want to tune it.

Response shape for multi-day forecasts

Open-Meteo returns hourly arrays keyed by variable name, which is more efficient than WeatherAPI's per-hour objects but requires a different deserialization approach. The Rust structs I had for WeatherAPI deserialization didn't map cleanly to the new shape. I ended up writing a custom Deserialize impl that zips the time array with each variable array, which was fine once I did it but took an afternoon of fighting the borrow checker.

Timestamp handling near DST transitions

Open-Meteo returns timestamps in UTC with no timezone field; WeatherAPI was returning local time with a timezone offset in the response. I had been relying on that offset to display times in the user's local zone. After the migration, timestamps were silently displaying in UTC for anyone not in UTC. This went unnoticed for two weeks because I do most of my testing in winter when my local time is UTC−8 and the offset is obvious in the output. It was only caught when someone in Australia filed an issue saying all the tide times were wrong.

The fix was to use the user's system timezone from tzdata and convert explicitly at display time, which is what I should have done from the start.

The accuracy improvement

I wasn't expecting Open-Meteo to be more accurate than WeatherAPI for offshore swell — I assumed it would be about the same. But I ran a three-week informal comparison between both APIs and NDBC buoy readings for significant wave height at station 46026 (San Francisco), and Open-Meteo was consistently closer to the observed values, especially for long-period northwest swell.

My guess is that ECMWF's wave model handles long-period swell propagation better than whatever WeatherAPI was using. I didn't dig deep enough to be sure, but the practical result is what matters: the surf output is meaningfully more useful now than it was in v0.3.

Lessons

The migration reinforced something I already knew but had to learn again: don't abstract over third-party APIs too early. I had wrapped the WeatherAPI response in a generic MarineConditions struct thinking it would make provider swaps easy. It didn't — the abstraction was at the wrong level. I should have kept the raw response types and only normalized at the very last step before display.

The timezone bug is just embarrassing. Test with users in non-UTC timezones.

← All posts