Race Conditions

A race-day forecast for the IRONMAN Ottawa bike course.

View the Ottawa 2026 archive

What was the problem?

I’m a competitive triathlete. For a couple of years, I’ve wanted to finish an IRONMAN in under 10 hours. In 2025, I finished IRONMAN Ottawa in 10 hours 16 minutes, 16 minutes off my target.

What was the idea?

The bike is the longest part of the race, so I wanted to understand how the wind would affect me. Being able to see the forecast wind on the bike course, at the time I’d ride each part, would help me manage my energy and be ready for a fast marathon.

What did I build?

I built it as a web app, because I wanted to see it on a big screen, from my laptop. That was the simplest approach. A Laravel app ran on a droplet I already rented at DigitalOcean. A job fetched the forecast every 15 minutes, so a visit never waited on the weather service. I shipped updates with a single command on my Mac.

Race Conditions in race week. When someone visits: the browser loads the page through the Cloudflare proxy, Nginx on the DigitalOcean VPS passes it to Laravel, and Laravel reads the last good forecast. Map tiles load straight from OpenStreetMap. Every 15 minutes: a refresh job fetches the Canadian GEM forecast from Open-Meteo, gets 25 hourly readings back, and saves them only if all 25 came back.

USER’S DEVICECLOUDFLAREDIGITALOCEAN VPSOUTSIDE SERVICES12reads3map tiles, straight from the browserBrowserCourse map + riderOpenStreetMapMap tilesProxyHTTPS in frontNginxTLS, rate limitsLaravel pagePHP-FPMLast good forecastJob writes, pages readsaves only if all 25 came backCfetchA25 hourly readingsBRefresh jobsystemd timerOpen-MeteoCanadian GEMnot involvednot involved

What happened?

The app gave me confidence heading into race day. I finished the IRONMAN in 9:38, well within my goal.

I also shared it with the IRONMAN Ottawa Facebook group. People liked it, but some said the temperatures looked off. That evening, I switched to the Canadian GEM model, and the forecast matched The Weather Network more closely. There were 178 visitors in race week, and 55 of them used the wind controls.

How I’d build it today

If I started over, I would build a fully static page on Cloudflare, with a scheduled Worker that keeps the last good forecast. Cloudflare can run the whole site on its own, without the monthly cost of a VPS to maintain. I would also store the course map tiles with the site, so visitors wouldn’t load them straight from OpenStreetMap. And I would polish the design to make it feel more modern.

Race Conditions, if I built it today. A proposal, not built. When someone visits: the browser loads a static page from Cloudflare, gets map tiles from R2 (built once from OpenStreetMap data), and asks a Worker for the forecast, which it reads from Workers KV. Every 15 minutes: a cron trigger runs a Worker that fetches the Canadian GEM forecast from Open-Meteo and saves it to KV only if all 25 hourly readings came back. There is no server to run.

USER’S DEVICECLOUDFLAREOUTSIDE SERVICES1GET /forecast2reads3map tilesBrowserCourse map + riderStatic pageNo serverCourse map tilesR2, from OSM dataOpenStreetMap: onlywhen building the tilesWorker · serveReads KVLast good forecastWorkers KVevery 15 minsaves only if all 25 came backCfetchA25 hourly readingsBCron triggerCloudflareWorker · refreshChecks 25 hoursOpen-MeteoCanadian GEMnot involved

Say hello

Have a project in mind, a role you think I’d be interested in, or a question about something I’ve built? Send me a note.

hello@victorc.dev