<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bakshi Iskandarov</title>
    <description>The latest articles on DEV Community by Bakshi Iskandarov (@bakshi).</description>
    <link>https://dev.to/bakshi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4180158%2Fe25d98de-dc19-43ee-9a40-8961877325d5.png</url>
      <title>DEV Community: Bakshi Iskandarov</title>
      <link>https://dev.to/bakshi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bakshi"/>
    <language>en</language>
    <item>
      <title>I built an aurora forecast site on Cloudflare Workers and hit every limit they have</title>
      <dc:creator>Bakshi Iskandarov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 18:21:51 +0000</pubDate>
      <link>https://dev.to/bakshi/i-built-an-aurora-forecast-site-on-cloudflare-workers-and-hit-every-limit-they-have-3882</link>
      <guid>https://dev.to/bakshi/i-built-an-aurora-forecast-site-on-cloudflare-workers-and-hit-every-limit-they-have-3882</guid>
      <description>&lt;p&gt;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: &lt;strong&gt;"will I actually see it tonight?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://auroravisible.com" rel="noopener noreferrer"&gt;Aurora Visible&lt;/a&gt; — one score per destination per night, computed from the four things that actually decide whether you'll see anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = 10 × darkness × geomagnetic × clear-sky × moonlight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router) via &lt;strong&gt;&lt;a href="https://opennext.js.org/cloudflare" rel="noopener noreferrer"&gt;@opennextjs/cloudflare&lt;/a&gt;&lt;/strong&gt; on Workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R2&lt;/strong&gt; for the ISR page cache, a &lt;strong&gt;Durable Object&lt;/strong&gt; queue for timed revalidation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,524 statically prerendered routes&lt;/strong&gt; (8 destinations × 12 months × 17 locales, plus compare pages, FAQ, guides)&lt;/li&gt;
&lt;li&gt;suncalc for darkness windows, NOAA SWPC + GFZ for geomagnetic data, Open-Meteo for clouds&lt;/li&gt;
&lt;li&gt;Plausible (cookieless) for analytics, Sentry for errors, Playwright + Vitest for tests&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  War story #1: one innocent &lt;code&gt;headers()&lt;/code&gt; call nuked my entire ISR setup
&lt;/h2&gt;

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

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

&lt;p&gt;On Cloudflare's free tier, a Worker invocation gets &lt;strong&gt;50 subrequests&lt;/strong&gt; — 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 &lt;code&gt;Too many subrequests by single Worker invocation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: make the 404 a client component that derives the locale from &lt;code&gt;usePathname()&lt;/code&gt;, 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.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  War story #2: the 10 ms CPU ceiling
&lt;/h2&gt;

&lt;p&gt;Errors kept coming after that fix. &lt;code&gt;wrangler tail --format json&lt;/code&gt; gave me the receipts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free tier allows &lt;strong&gt;10 ms of CPU per invocation&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story #3: subrequest arithmetic
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/v1/forecast?latitude=69.6,68.4,64.1,…&amp;amp;longitude=18.9,18.8,-21.9,…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Smaller lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;suncalc 2.x returns degrees, not radians.&lt;/strong&gt; Changed from 1.x. I lost a real bug to "fixing" the missing conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin your Next.js version.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic e2e beats mocking.&lt;/strong&gt; All upstream feeds go through one fetch module with a fixtures mode (&lt;code&gt;SOURCE_FIXTURES=1&lt;/code&gt;) and a pinned clock (&lt;code&gt;FIXED_NOW=…&lt;/code&gt;). 53 Playwright tests run against a real production build with zero network and zero flake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wrangler tail --format json&lt;/code&gt; is the single most useful debugging tool on Workers.&lt;/strong&gt; Dashboard error counts tell you nothing; the tail gives you per-invocation CPU time, outcomes, and exceptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Was Workers the right call?
&lt;/h2&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The site is live at &lt;strong&gt;&lt;a href="https://auroravisible.com" rel="noopener noreferrer"&gt;auroravisible.com&lt;/a&gt;&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;Happy to answer anything about the scoring model, the i18n setup (17 locales on one &lt;code&gt;[locale]&lt;/code&gt; tree), or the Cloudflare specifics in the comments.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
