DEV Community

Cover image for I built an aurora forecast site on Cloudflare Workers and hit every limit they have
Bakshi Iskandarov
Bakshi Iskandarov

Posted on

I built an aurora forecast site on Cloudflare Workers and hit every limit they have

Every aurora app answers the wrong question. They show you Kp indexes, solar wind speed, magnetometer plots — great if you're a space-weather nerd. But a traveler standing in Tromsø asks something simpler: "will I actually see it tonight?"

The honest answer depends as much on clouds, darkness, and the moon as it does on the sun. A Kp 7 storm is worthless under overcast skies. A quiet Kp 2 night in Abisko in December can be magical.

So I built Aurora Visible — one score per destination per night, computed from the four things that actually decide whether you'll see anything:

score = 10 × darkness × geomagnetic × clear-sky × moonlight
Enter fullscreen mode Exit fullscreen mode

Eight destinations (Tromsø, Abisko, Reykjavík, Fairbanks, Yellowknife…), a verdict for tonight updated every 30 minutes from NOAA and Open-Meteo data, monthly odds computed from 30 years of geomagnetic history, and month-by-month pages for trip planning. In 17 languages, because aurora tourists come from everywhere.

This post is about what it took to run that on Cloudflare Workers — because I hit, in one day, pretty much every limit the platform has.

The stack

  • Next.js 16 (App Router) via @opennextjs/cloudflare on Workers
  • R2 for the ISR page cache, a Durable Object queue for timed revalidation
  • 2,524 statically prerendered routes (8 destinations × 12 months × 17 locales, plus compare pages, FAQ, guides)
  • suncalc for darkness windows, NOAA SWPC + GFZ for geomagnetic data, Open-Meteo for clouds
  • Plausible (cookieless) for analytics, Sentry for errors, Playwright + Vitest for tests

The appeal of Workers: the free tier allows commercial use, and the paid tier is $5/month. The catch: it is not a Node server, and the limits are real.

War story #1: one innocent headers() call nuked my entire ISR setup

I wanted the 404 page localized. not-found.tsx boundaries don't receive route params in Next.js, so I had my middleware forward the locale in an x-locale header and read it with headers() in the boundary. Clean, right?

What I didn't realize: a dynamic API call anywhere in a layout segment opts every non-force-static page in that segment out of static generation. My home page, storm tracker, and destination pages — all revalidate: 1800 ISR — silently became fully dynamic. Every request did a complete React server render with ~10 upstream data fetches.

On Cloudflare's free tier, a Worker invocation gets 50 subrequests — and with OpenNext, every R2 read, every Cache API call, every Durable Object ping counts. Cache-miss renders blew past 50 and the site started throwing Too many subrequests by single Worker invocation.

The fix: make the 404 a client component that derives the locale from usePathname(), with a tiny standalone map of just the 404 strings (importing the full message catalogs would have shipped ~460 KB of translations to every visitor). A unit test keeps the map in sync with the catalogs.

Diagnostic that saved me: check .next/prerender-manifest.json after builds. The route count dropping from 2,490 to 2,303 is the tell — I now assert on it.

War story #2: the 10 ms CPU ceiling

Errors kept coming after that fix. wrangler tail --format json gave me the receipts:

outcome: exceededCpu  cpu: 10ms  /az/storm
outcome: ok           cpu: 19ms  /de/kiruna   ← survived on burst tolerance
outcome: ok           cpu: 991ms /en/tromso   ← a full ISR regeneration
Enter fullscreen mode Exit fullscreen mode

The free tier allows 10 ms of CPU per invocation. A Next.js page render through the OpenNext server path costs 15–20 ms even when it's mostly serving from cache, and an ISR regeneration costs 50–1000 ms. Whether a given request survived was literally luck — Cloudflare's enforcement allows bursts, so identical requests would alternate between 200 and 503.

There is no code fix for this. React SSR does not fit in 10 ms. The $5/month paid plan raises the ceiling to 30 seconds and the errors stopped within a minute of upgrading. If you're putting any SSR framework on Workers: budget the $5 from day one and save yourself the afternoon.

War story #3: subrequest arithmetic

Even on the paid plan, subrequests are worth respecting. My forecast engine fetched Open-Meteo once per destination — 8 fetches, each with cache reads/writes around it. Open-Meteo accepts comma-separated coordinate lists:

/v1/forecast?latitude=69.6,68.4,64.1,…&longitude=18.9,18.8,-21.9,…
Enter fullscreen mode Exit fullscreen mode

One request, results in order, one cache entry for the whole site. A page regeneration went from ~40 subrequests to ~12.

Smaller lessons

  • suncalc 2.x returns degrees, not radians. Changed from 1.x. I lost a real bug to "fixing" the missing conversion.
  • Pin your Next.js version. Next 16.4 emits a build manifest file the Cloudflare adapter doesn't whitelist yet; the worker 500s on boot. I'm pinned to 16.3.8 until the adapter catches up.
  • Deterministic e2e beats mocking. All upstream feeds go through one fetch module with a fixtures mode (SOURCE_FIXTURES=1) and a pinned clock (FIXED_NOW=…). 53 Playwright tests run against a real production build with zero network and zero flake.
  • wrangler tail --format json is the single most useful debugging tool on Workers. Dashboard error counts tell you nothing; the tail gives you per-invocation CPU time, outcomes, and exceptions.

Was Workers the right call?

For this site — yes, now that I understand the shape of it. 2,524 prerendered pages served from R2 behind Cloudflare's edge, background revalidation every 30 minutes, global latency that makes Lighthouse happy, for $5/month at any realistic traffic level. The same thing on Vercel would be $20/month before I could legally put an affiliate link on it (Hobby tier forbids commercial use).

But the mental model is different from a Node server, and the free tier is for APIs and tiny pages, not SSR frameworks. Go in knowing that.

The site is live at auroravisible.com — aurora season just started and we're near solar maximum, so if you've ever wanted to see the northern lights, the next few months are the best window in a decade. I'd genuinely love feedback, especially from anyone who's run OpenNext on Workers at larger scale.

Happy to answer anything about the scoring model, the i18n setup (17 locales on one [locale] tree), or the Cloudflare specifics in the comments.

Top comments (0)