<?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: cloudflare</title>
    <description>The latest articles tagged 'cloudflare' on DEV Community.</description>
    <link>https://dev.to/t/cloudflare</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/cloudflare"/>
    <language>en</language>
    <item>
      <title>How to Automatically Start a Cloudflare Tunnel When You Sign In to Windows</title>
      <dc:creator>Travis Calvert</dc:creator>
      <pubDate>Tue, 20 Oct 2026 22:45:49 +0000</pubDate>
      <link>https://dev.to/travis_calvert/how-to-automatically-start-a-cloudflare-tunnel-when-you-sign-in-to-windows-1095</link>
      <guid>https://dev.to/travis_calvert/how-to-automatically-start-a-cloudflare-tunnel-when-you-sign-in-to-windows-1095</guid>
      <description>&lt;p&gt;If you use Cloudflare Tunnel to expose a local development server, self-hosted application, or internal dashboard, you've probably encountered this problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything works until you restart Windows.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your application might still be running, but the Cloudflare Tunnel process isn't. Suddenly, your public hostname is unreachable until you manually launch &lt;code&gt;cloudflared&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;Here's how to automate that process using Windows Task Scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A typical Cloudflare Tunnel setup uses a command like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cloudflared.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tunnel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;YOUR_TUNNEL_TOKEN&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works perfectly well in an interactive terminal.&lt;/p&gt;

&lt;p&gt;But closing the terminal, signing out, or restarting Windows stops the process.&lt;/p&gt;

&lt;p&gt;You can solve this by configuring Windows to launch the tunnel automatically when you sign in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a startup script
&lt;/h2&gt;

&lt;p&gt;Create a PowerShell script named &lt;code&gt;Start-Tunnel.ps1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cloudflared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Tools\cloudflared.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$tokenFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\CloudflareTunnel\token.txt"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cloudflared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudflared.exe was not found."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tokenFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tunnel token file was not found."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tokenFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tunnel token is empty."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cloudflared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tunnel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the executable path to match your installation.&lt;/p&gt;

&lt;p&gt;The script reads the tunnel token from a separate local file instead of embedding it directly in the script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Your tunnel token is a credential. Don't commit it to GitHub, include it in screenshots, or share it in support requests. Restrict access to the token file to your Windows account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure Windows Task Scheduler
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Task Scheduler&lt;/strong&gt; and select &lt;strong&gt;Create Task&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;General&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the task a descriptive name, such as &lt;code&gt;Cloudflare Tunnel Startup&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Run only when user is logged on&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use your normal Windows account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under &lt;strong&gt;Triggers&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a trigger that runs &lt;strong&gt;At log on&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select your Windows account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under &lt;strong&gt;Actions&lt;/strong&gt;, create a new action:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program/script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powershell.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add arguments:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\Start-Tunnel.ps1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the script path with your actual location.&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Settings&lt;/strong&gt;, review the options that control whether the task can start again while an existing instance is running.&lt;/p&gt;

&lt;p&gt;Save the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Test it
&lt;/h2&gt;

&lt;p&gt;Right-click the task and select &lt;strong&gt;Run&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Check Task Manager for the &lt;code&gt;cloudflared.exe&lt;/code&gt; process.&lt;/p&gt;

&lt;p&gt;Then verify that your public hostname actually reaches your local application.&lt;/p&gt;

&lt;p&gt;A running process alone doesn't prove the tunnel is connected successfully.&lt;/p&gt;

&lt;p&gt;Finally, sign out and sign back in to confirm that the task launches automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about running the tunnel before login?
&lt;/h2&gt;

&lt;p&gt;A sign-in task starts only after the specified user logs on.&lt;/p&gt;

&lt;p&gt;If you need the tunnel to be available immediately after Windows boots, before anyone signs in, you'll need a different approach, such as a properly configured Windows service.&lt;/p&gt;

&lt;p&gt;That distinction matters for unattended servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this basic approach gets complicated
&lt;/h2&gt;

&lt;p&gt;Task Scheduler handles the initial launch, but a production-quality setup involves more than just running a PowerShell script.&lt;/p&gt;

&lt;p&gt;You may also want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protection against duplicate launchers&lt;/li&gt;
&lt;li&gt;A predictable way to stop the tunnel&lt;/li&gt;
&lt;li&gt;Logging for troubleshooting&lt;/li&gt;
&lt;li&gt;Secure token storage&lt;/li&gt;
&lt;li&gt;Clean installation and removal&lt;/li&gt;
&lt;li&gt;Reliable cleanup of child processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These details can take longer to get right than the initial scheduled task.&lt;/p&gt;

&lt;h2&gt;
  
  
  A ready-made alternative
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Cloudflare Tunnel Auto-Start Kit for Windows&lt;/strong&gt; under Blacktop Logic to package the Windows sign-in approach into a reusable toolkit.&lt;/p&gt;

&lt;p&gt;It includes PowerShell installation, startup, status, and uninstall scripts, along with process management and logging.&lt;/p&gt;

&lt;p&gt;It is designed for &lt;strong&gt;user sign-in startup&lt;/strong&gt;, not as a replacement for a Windows service.&lt;/p&gt;

&lt;p&gt;The kit is available as a one-time $19 purchase:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://payhip.com/b/FnwaJ" rel="noopener noreferrer"&gt;https://payhip.com/b/FnwaJ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're comfortable maintaining your own scheduled task, the manual approach above may be all you need.&lt;/p&gt;

&lt;p&gt;If you'd rather avoid building and troubleshooting the supporting scripts yourself, the kit is another option.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published by Blacktop Logic — practical software utilities for developers and self-hosters.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>windows</category>
      <category>powershell</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Urdu voice notes to text on a free tier: Whisper pinned to Urdu, Roman Urdu without drift, and an honest error rate</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:16:54 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/urdu-voice-notes-to-text-on-a-free-tier-whisper-pinned-to-urdu-roman-urdu-without-drift-and-an-21gf</link>
      <guid>https://dev.to/lowkeybuilds/urdu-voice-notes-to-text-on-a-free-tier-whisper-pinned-to-urdu-roman-urdu-without-drift-and-an-21gf</guid>
      <description>&lt;p&gt;Pakistan runs on WhatsApp voice notes. WhatsApp now writes transcripts for some languages, but Urdu is not in any list of supported languages I could find, and a Hindi transcript of Urdu speech comes out in Devanagari, which most Pakistanis do not read.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://sunlo.appin60.com" rel="noopener noreferrer"&gt;Sunlo&lt;/a&gt;, a free web app: drop in a voice note, get Urdu script, Roman Urdu (the way people actually type on WhatsApp) and an English translation. Disclosure: I made it, and this post is how it works, including what it gets wrong.&lt;/p&gt;

&lt;p&gt;The constraints: no budget, no audio stored anywhere, and a cheap Android phone as the main client.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Whisper on Workers AI, language pinned to Urdu
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers AI hosts &lt;code&gt;whisper-large-v3-turbo&lt;/code&gt;. The one setting that matters is &lt;code&gt;language: 'ur'&lt;/code&gt;. Left to auto-detect, Whisper often decides Urdu speech is Hindi (the spoken languages are close) and writes Devanagari.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cf/openai/whisper-large-v3-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base64Audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// WhatsApp .opus works as-is&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// never auto-detect for Urdu&lt;/span&gt;
  &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transcribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vad_filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// skips silence in voice notes&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WhatsApp's &lt;code&gt;.opus&lt;/code&gt; files go straight in; no ffmpeg needed on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A hard daily cap, enforced before the model runs
&lt;/h2&gt;

&lt;p&gt;The free Workers AI allocation is 10,000 "neurons" a day. Whisper turbo costs about 46 neurons per audio minute ($0.00051/min at $0.011 per 1k neurons). The Worker keeps a per-day counter in D1 and &lt;strong&gt;reserves&lt;/strong&gt; the estimated cost before calling the model, so parallel requests cannot overshoot, then corrects it with the real duration afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;est&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;46.4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT neurons FROM budget WHERE day=?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;neurons&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;est&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;daily_cap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`INSERT INTO budget (day, neurons) VALUES (?, ?)
  ON CONFLICT(day) DO UPDATE SET neurons = neurons + excluded.neurons`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;est&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-visitor limits use a salted daily hash of the IP, so nothing identifying is kept. Audio and text are never written anywhere; D1 only holds counts, durations and timings.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Roman Urdu: transliterate, do not translate
&lt;/h2&gt;

&lt;p&gt;Roman Urdu and English come from one call to Llama 4 Scout (about 11 neurons for a short note). The first prompt said "write it in Roman Urdu" and the model drifted into English words ("patient ko dusre se alag karna include hai"). Saying TRANSLITERATE, with two examples of how people type, fixed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"roman"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TRANSLITERATE,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translate.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Write&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Urdu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;word&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;same&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Urdu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;word&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Latin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;letters,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;way&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Pakistanis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;WhatsApp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main kal aaunga, aap fikar na karein"&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Llama 3.1 8B was cheaper but changed words ("aaj", today, became "kal"). Scout, Llama 3.3 70B and gpt-oss-120b were all fine on clean text.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Long recordings: cut at silence, in the browser
&lt;/h2&gt;

&lt;p&gt;Lectures and interviews are longer than one request should be. The browser decodes the file with &lt;code&gt;AudioContext({ sampleRate: 16000 })&lt;/code&gt;, which also resamples to what Whisper wants, then cuts it into ~150-second pieces. Each cut goes at the quietest 50 ms window in the last quarter of the piece, so words are not split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bestE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bestE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pieces are transcribed one by one, joined, and then translated in a single pass, because translating fragments separately produced nonsense at the seams.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Share target: one tap from WhatsApp
&lt;/h2&gt;

&lt;p&gt;On Android, an installed PWA can appear in WhatsApp's Share sheet. The manifest declares a &lt;code&gt;share_target&lt;/code&gt; with a multipart POST; the service worker catches it, stashes the file in Cache Storage and redirects to the page, which picks it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"share_target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/share-target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enctype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multipart/form-data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"audio/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".opus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".ogg"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How accurate is it?
&lt;/h2&gt;

&lt;p&gt;I ran the first 30 clips of the FLEURS Urdu test set (28 downloaded) through the live API, encoded as Opus like a WhatsApp note:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Word error rate&lt;/th&gt;
&lt;th&gt;Character error rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;20.8%&lt;/td&gt;
&lt;td&gt;8.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White noise at 15 dB SNR&lt;/td&gt;
&lt;td&gt;23.0%&lt;/td&gt;
&lt;td&gt;9.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That counts spelling variants and "70" against "ستر" as errors, so the text reads better than one wrong word in five suggests. Real voice notes are harder than read sentences.&lt;/p&gt;

&lt;p&gt;The failure I worry about most: in a noisy test, "jald rihaa" (released quickly) was heard as "jal raha" (burning), and every model I tried, including the 70B and 120B ones, then translated the sentence as hostages being set on fire. A recognition slip becomes an alarming English sentence. The prompt now tells the model not to invent dramatic meanings, but the honest fix is in the interface: every result says it is a machine transcript and to check money, medicine, addresses and times against the audio. The method and examples are on the &lt;a href="https://sunlo.appin60.com/accuracy.html" rel="noopener noreferrer"&gt;accuracy page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sunlo.appin60.com" rel="noopener noreferrer"&gt;sunlo.appin60.com&lt;/a&gt; is free, needs no account, and handles notes up to 10 minutes. If you speak Urdu, I would like to hear where it goes wrong: &lt;a href="mailto:sunlo@appin60.com"&gt;sunlo@appin60.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>ai</category>
      <category>whisper</category>
    </item>
    <item>
      <title>Your Cloudflare Edge Is in the Wrong Country — colo= vs loc= and a 6-Second Page That Became 1 Second</title>
      <dc:creator>Daanial Farrukh</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:42:21 +0000</pubDate>
      <link>https://dev.to/daanial/your-cloudflare-edge-is-in-the-wrong-country-colo-vs-loc-and-a-6-second-page-that-became-1-314j</link>
      <guid>https://dev.to/daanial/your-cloudflare-edge-is-in-the-wrong-country-colo-vs-loc-and-a-6-second-page-that-became-1-314j</guid>
      <description>&lt;p&gt;&lt;em&gt;A presales war story about a slow site behind Cloudflare — the edge location almost nobody checks — and the two-line test I now run before anyone touches the server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The complaint came in the usual way. “The website is slow. Can you look at the server?”&lt;/p&gt;

&lt;p&gt;It was a public-sector portal. The server was in Pakistan. Almost every visitor was in Pakistan. And the site sat behind Cloudflare, because “Cloudflare makes it faster.”&lt;/p&gt;

&lt;p&gt;The team had already done the normal things. They checked CPU. They checked memory. They restarted Apache once, just in case.&lt;/p&gt;

&lt;p&gt;“The server is fine,” they said. “But the page still takes six seconds.”&lt;/p&gt;

&lt;p&gt;I asked one question. “How long does it take if you skip Cloudflare and hit the server directly?”&lt;/p&gt;

&lt;p&gt;Silence. Nobody had tried.&lt;/p&gt;

&lt;p&gt;So we tried. Same laptop, same browser, same page — pointed straight at the origin IP.&lt;/p&gt;

&lt;p&gt;About one second.&lt;/p&gt;

&lt;p&gt;Six seconds through Cloudflare. One second without it. Same server, same content, same moment.&lt;/p&gt;

&lt;p&gt;“So Cloudflare is slowing us down?”&lt;/p&gt;

&lt;p&gt;Not Cloudflare itself. Where Cloudflare was serving them from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap — The Edge You Never Look At
&lt;/h2&gt;

&lt;p&gt;That’s the trap. A slow site behind a CDN does not show up as a slow server. Every server metric is green, so everyone keeps looking at the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem was not on the server. It was on the map.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Cloudflare “colo” Actually Is
&lt;/h2&gt;

&lt;p&gt;Cloudflare runs data centres all over the world. Each one is called a &lt;strong&gt;colo&lt;/strong&gt; (short for colocation), and each has a three-letter airport code: LHE for Lahore, DXB for Dubai, MCT for Muscat, SIN for Singapore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The colo is the Cloudflare data centre that your visitor actually talks to.&lt;/strong&gt; It is not a setting on your site. Cloudflare uses Anycast: the same IP address is announced from every data centre, and the visitor’s internet provider decides — through its own routing — which one the traffic lands on.&lt;/p&gt;

&lt;p&gt;When the colo is close to both the visitor and the origin, the path is short:&lt;/p&gt;

&lt;p&gt;Visitor (Pakistan) → Cloudflare colo (Pakistan) → Origin (Pakistan)&lt;/p&gt;

&lt;p&gt;When the visitor’s provider sends traffic abroad, the same request looks like this:&lt;/p&gt;

&lt;p&gt;Visitor (Pakistan) → Cloudflare colo (Oman) → Origin (Pakistan) → Cloudflare colo (Oman) → Visitor (Pakistan)&lt;/p&gt;

&lt;p&gt;That is an international round trip for something that should never have left the country. Once is fine. But a modern page is not one request.&lt;/p&gt;

&lt;p&gt;This portal’s homepage made about &lt;strong&gt;140 requests&lt;/strong&gt; — 18 MB, mostly unoptimised images and about 15 separate CSS files. Every small CSS or JS file took &lt;strong&gt;300–600 ms through Cloudflare&lt;/strong&gt; and about &lt;strong&gt;30 ms direct&lt;/strong&gt;. Multiply by 140 and the six seconds explain themselves.&lt;/p&gt;

&lt;p&gt;This pattern is common where Cloudflare has weak local peering. In Pakistan, visitors on non-Enterprise plans are often served from Muscat, Dubai or Singapore. Cloudflare’s own documentation describes Pakistani traffic being routed to SIN, MCT and DXB, driven by local provider peering and submarine-cable damage.&lt;/p&gt;

&lt;p&gt;Where you see it most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Countries where the main providers do not peer well with Cloudflare locally.&lt;/li&gt;
&lt;li&gt;Sites whose origin and audience are in the same country.&lt;/li&gt;
&lt;li&gt;Heavy pages with many small, uncached files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the Usual Check Misses It
&lt;/h2&gt;

&lt;p&gt;There are three reasons nobody found it.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;server monitoring cannot see it.&lt;/strong&gt; The origin answered every request quickly. CPU, memory and Apache were all healthy, because they were.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;the colo is per visitor, not per site.&lt;/strong&gt; There is no “site colo” in the dashboard. A visitor on one provider may land in Lahore, while a visitor on another lands in Muscat. The person testing from the office may get a different path from the people complaining.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;it changes over time.&lt;/strong&gt; Cloudflare steers traffic for congestion, maintenance and capacity, and its documentation says it may route self-service plans to other data centres when peering is missing or load is high. A test from last month proves nothing about today.&lt;/p&gt;

&lt;p&gt;Here’s the part that trips people up: the first instinct is “the cache is not working”. But direct-to-origin has no cache at all — and it was still six times faster. &lt;strong&gt;The cost was the route, not the cache.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Fix This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Read colo= and loc= — the two-line test
&lt;/h3&gt;

&lt;p&gt;Every Cloudflare-proxied site answers on a special path. Open it in the browser of a real visitor, or run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://example.com/cdn-cgi/trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;colo=MCT
loc=PK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loc=&lt;/code&gt; is the visitor’s country. &lt;code&gt;colo=&lt;/code&gt; is the Cloudflare data centre serving them. In this case: visitor in Pakistan, edge in Muscat, Oman.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;colo=&lt;/code&gt; is in the same country as &lt;code&gt;loc=&lt;/code&gt; and your origin, the route is fine and the slowness is somewhere else. If it is in another country, keep going.&lt;/p&gt;

&lt;p&gt;Plain takeaway: &lt;strong&gt;two lines tell you whether your traffic is leaving the country.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Time one small file both ways
&lt;/h3&gt;

&lt;p&gt;Pick a small static file, like a CSS file. Time it through Cloudflare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://example.com/wp-includes/css/dist/block-library/style.min.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then time the same file directly from the origin. &lt;code&gt;--resolve&lt;/code&gt; sends the request to the origin IP while keeping the hostname, so TLS and virtual hosts still work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--resolve&lt;/span&gt; example.com:443:&amp;lt;ORIGIN_IP&amp;gt; https://example.com/wp-includes/css/dist/block-library/style.min.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;-k&lt;/code&gt; if the origin uses a Cloudflare Origin Certificate, because that certificate is only trusted by Cloudflare.&lt;/p&gt;

&lt;p&gt;On this portal the numbers were roughly 500 ms per file through the edge against 30 ms direct. That difference, times 140 files, is the whole problem.&lt;/p&gt;

&lt;p&gt;Plain takeaway: &lt;strong&gt;if one static file is ten times slower through the edge, the page will be too.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Confirm with the full page
&lt;/h3&gt;

&lt;p&gt;One file can lie. A full page cannot. Point your own machine at the origin with a hosts-file entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;ORIGIN_IP&amp;gt;  example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows it lives in &lt;code&gt;C:\Windows\System32\drivers\etc\hosts&lt;/code&gt;, on Linux and macOS in &lt;code&gt;/etc/hosts&lt;/code&gt;. Load the page with DevTools open (Network tab, cache disabled) and note the total load time. Remove the line and load it again through Cloudflare.&lt;/p&gt;

&lt;p&gt;This step is where most tutorials stop and where mine starts. Remember to delete the hosts entry afterwards — otherwise you will keep testing the origin and wonder why the “fix” worked only for you.&lt;/p&gt;

&lt;p&gt;Our result: about 1 second direct, about 6 seconds through the domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Log CF-Ray to see what real visitors get
&lt;/h3&gt;

&lt;p&gt;The trace shows only your own colo. To see what your visitors get, log the &lt;code&gt;CF-Ray&lt;/code&gt; header at the origin. Its value ends with the colo code, for example &lt;code&gt;8a1b2c3d4e5f6789-MCT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Check it on one response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; cf-ray
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To log it for every visitor, add it to the access log format. In nginx the variable is &lt;code&gt;$http_cf_ray&lt;/code&gt;; in Apache it is &lt;code&gt;%{CF-Ray}i&lt;/code&gt;. After a day, count the codes at the end of each value. If most of your traffic ends in a foreign code, you have your answer from real users, not from one laptop.&lt;/p&gt;

&lt;p&gt;Plain takeaway: &lt;strong&gt;there is no single site colo — measure it across real visitors.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Choose the fix — and be honest about the trade-off
&lt;/h3&gt;

&lt;p&gt;You cannot force the colo on a normal plan. Only the Enterprise plan can pin traffic to the nearest data centre. So the choices are these:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — cache static files at the edge.&lt;/strong&gt; This removes the colo → origin leg for cached files. It does not remove the visitor → colo leg, so a foreign colo is still slower than local direct traffic. Worth doing anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — make the page lighter.&lt;/strong&gt; Combine the 15 CSS files, compress the 18 MB of images, cut the request count. Fewer round trips means less damage from each slow one. Worth doing anyway, too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C — DNS-only (grey cloud).&lt;/strong&gt; In the Cloudflare dashboard, go to DNS → Records, edit the record and switch Proxy status from Proxied to DNS only. Visitors then go straight to the origin over the local network.&lt;/p&gt;

&lt;p&gt;This customer chose Option C for a domestic audience. It is the fastest, but it is a real trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare’s DDoS protection and WAF no longer sit in front of the site.&lt;/li&gt;
&lt;li&gt;The origin IP is now public in DNS.&lt;/li&gt;
&lt;li&gt;The origin must already have a publicly trusted certificate. A Cloudflare Origin Certificate will give every visitor a browser error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the certificate &lt;strong&gt;before&lt;/strong&gt; you flip the switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; &amp;lt;ORIGIN_IP&amp;gt;:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the issuer is a public authority like Let’s Encrypt or Sectigo and the dates are valid, you are safe. If it says Cloudflare Origin, fix the certificate first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Verify the switch
&lt;/h3&gt;

&lt;p&gt;After the change, the domain must resolve to the origin, not to Cloudflare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nslookup example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see your origin IP. If you still see Cloudflare addresses, wait for the old DNS record to expire and test again.&lt;/p&gt;

&lt;p&gt;Then the trace must disappear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://example.com/cdn-cgi/trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 404 here is good news. &lt;strong&gt;This error is not a bug. It is the verification.&lt;/strong&gt; The path only exists on Cloudflare, so a 404 proves traffic no longer passes through it.&lt;/p&gt;

&lt;p&gt;Finally, load the full page again from a normal visitor connection. Our portal went from about 6 seconds to about 1 second, with no browser certificate warnings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Re-protect the origin — the step people skip
&lt;/h3&gt;

&lt;p&gt;Here is the reversal nobody warns you about. Many hardening guides tell you to lock port 443 so it only accepts Cloudflare’s IP ranges. That is correct when the site is proxied. &lt;strong&gt;After going DNS-only, that same rule blocks every real visitor&lt;/strong&gt;, because they now arrive from their own IPs.&lt;/p&gt;

&lt;p&gt;So the protection has to move to the origin: the server firewall, connection limits, rate limiting, and fail2ban or CSF for abuse. Plan that the same day you grey-cloud the record, not after the first attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson — A Short Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edge location&lt;/strong&gt; — &lt;code&gt;curl https://example.com/cdn-cgi/trace&lt;/code&gt;, compare &lt;code&gt;colo=&lt;/code&gt; with &lt;code&gt;loc=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge vs direct, one file&lt;/strong&gt; — the two &lt;code&gt;curl -w&lt;/code&gt; timings above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge vs direct, full page&lt;/strong&gt; — hosts-file test with DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real visitor colos&lt;/strong&gt; — log &lt;code&gt;CF-Ray&lt;/code&gt; and count the colo codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page weight&lt;/strong&gt; — DevTools Network tab: request count and total MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin certificate&lt;/strong&gt; — the &lt;code&gt;openssl s_client&lt;/code&gt; check before any grey-cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin firewall after DNS-only&lt;/strong&gt; — no Cloudflare-only rule left on 443.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I Tell Every Customer Now
&lt;/h2&gt;

&lt;p&gt;A CDN is a promise about distance. It only keeps that promise if the edge is close to the people using it.&lt;/p&gt;

&lt;p&gt;So before anyone opens &lt;code&gt;top&lt;/code&gt; on a “slow server” behind Cloudflare, I ask one question:&lt;/p&gt;

&lt;p&gt;“Which colo are your visitors actually landing on?”&lt;/p&gt;

&lt;p&gt;If the answer is a city in their own country, they’re in good shape, and we go back to the server. If the answer is “What’s a colo?”, we’re not done yet.&lt;/p&gt;

&lt;p&gt;I write about the questions I ask in real customer rooms — the ones that change architectures and surface the problems no one else noticed. If you found this useful, drop your email below and I’ll send the next one when it’s ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is my website slower with Cloudflare than without it?
&lt;/h3&gt;

&lt;p&gt;The most common reason is the route. If your visitors are served by a Cloudflare data centre in another country while your server is in their own country, every uncached request makes an international round trip. Compare &lt;code&gt;colo=&lt;/code&gt; and &lt;code&gt;loc=&lt;/code&gt; at &lt;code&gt;/cdn-cgi/trace&lt;/code&gt; to check.&lt;/p&gt;

&lt;h3&gt;
  
  
  What do colo and loc mean in Cloudflare’s cdn-cgi/trace?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;colo&lt;/code&gt; is the three-letter airport code of the Cloudflare data centre that served your request, such as MCT for Muscat. &lt;code&gt;loc&lt;/code&gt; is the country Cloudflare thinks you are in. If they are in different countries, your traffic is leaving the country before it reaches your site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I choose which Cloudflare data centre serves my visitors?
&lt;/h3&gt;

&lt;p&gt;Not on the Free, Pro or Business plans. Cloudflare uses Anycast, so the visitor’s internet provider decides which data centre their traffic reaches. Only the Enterprise plan can pin traffic to the closest data centre.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will caching fix a slow Cloudflare colo?
&lt;/h3&gt;

&lt;p&gt;Partly. Caching static files removes the trip from the colo to your origin, but the visitor still has to reach the distant colo. Caching and a lighter page reduce the damage; they do not make a foreign edge as fast as a local direct path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to switch a Cloudflare record to DNS only?
&lt;/h3&gt;

&lt;p&gt;It is safe if you accept the trade-offs and prepare for them. You lose Cloudflare’s DDoS protection and WAF, your origin IP becomes public, and the origin needs a publicly trusted certificate. Also remove any firewall rule that only allows Cloudflare IPs, or real visitors will be blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I see which Cloudflare colo my real visitors use?
&lt;/h3&gt;

&lt;p&gt;Log the &lt;code&gt;CF-Ray&lt;/code&gt; request header at your origin. Its value ends with the colo code. In nginx use &lt;code&gt;$http_cf_ray&lt;/code&gt;, in Apache use &lt;code&gt;%{CF-Ray}i&lt;/code&gt;, then count the codes across a day of traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://quabin.com/wp-content/uploads/2026/10/quabin-cloudflare-colo-wrong-country-slow-site.pdf" rel="noopener noreferrer"&gt;Download the PDF&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://quabin.com/cloudflare-colo-wrong-country-slow-site/" rel="noopener noreferrer"&gt;quabin.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>performance</category>
      <category>cdn</category>
      <category>devops</category>
    </item>
    <item>
      <title>My CDN Comparison: Cloudflare vs Fastly vs BunnyCDN for Business</title>
      <dc:creator>Veduis</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:47:41 +0000</pubDate>
      <link>https://dev.to/veduis/my-cdn-comparison-cloudflare-vs-fastly-vs-bunnycdn-for-business-35ef</link>
      <guid>https://dev.to/veduis/my-cdn-comparison-cloudflare-vs-fastly-vs-bunnycdn-for-business-35ef</guid>
      <description>&lt;p&gt;Content delivery networks have become key infrastructure for any business with a web presence. I've seen CDNs cache content at edge locations worldwide, reducing latency for global users and protecting origin servers from traffic spikes. The difference between a well-configured CDN and none can mean seconds of load time and significant conversion impact.&lt;/p&gt;

&lt;p&gt;Choosing between CDN providers involves trade-offs between performance, features, ease of use, and cost. In this comparison, I examine three leading options that serve different segments of the market: Cloudflare's thorough platform, Fastly's developer-focused edge computing, and BunnyCDN's cost-effective simplicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How CDNs Improve Performance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet0sxalhih97iouvp6u4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet0sxalhih97iouvp6u4.png" alt="Infographic map visualizing global CDN edge locations and data transmission" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before comparing providers, I find that understanding CDN mechanics helps evaluate their differences meaningfully.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge caching&lt;/strong&gt; - CDNs store copies of static assets at servers distributed globally. Users download files from nearby edge locations rather than distant origin servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection improvement&lt;/strong&gt; - CDNs maintain improved connections to origins and use protocols like HTTP/3 to accelerate delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression&lt;/strong&gt; - Automatic compression of text assets reduces transfer sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image improvement&lt;/strong&gt; - Many CDNs transform images on the fly, serving appropriate formats and sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDoS protection&lt;/strong&gt; - Distributed infrastructure absorbs attack traffic before it reaches origins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL/TLS termination&lt;/strong&gt; - CDNs handle encryption at the edge, reducing origin server load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The performance impact depends on visitor distribution and content characteristics. Sites with global audiences and substantial static content see the largest improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare: The Thorough Platform
&lt;/h2&gt;

&lt;p&gt;I frequently recommend &lt;a href="https://www.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;, which has evolved from a CDN into a thorough edge platform. The free tier makes it accessible to sites of any size, while enterprise features serve the largest properties.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Characteristics
&lt;/h3&gt;

&lt;p&gt;Cloudflare operates one of the largest networks with points of presence in over 300 cities. This density ensures most users connect to nearby edge servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark results (median values):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to first byte: 45ms (cached content)&lt;/li&gt;
&lt;li&gt;Global latency variance: Low&lt;/li&gt;
&lt;li&gt;Cache hit ratio: Depends on configuration&lt;/li&gt;
&lt;li&gt;HTTP/3 support: Full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudflare's Argo Smart Routing improves paths between edge and origin, improving performance for dynamic content that cannot be cached. This feature adds cost but delivers measurable latency reduction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Security suite:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web Application Firewall with managed rulesets&lt;/li&gt;
&lt;li&gt;DDoS protection included at all tiers&lt;/li&gt;
&lt;li&gt;Bot management and rate limiting&lt;/li&gt;
&lt;li&gt;Zero Trust access controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic image improvement (Polish, Mirage)&lt;/li&gt;
&lt;li&gt;JavaScript and CSS minification&lt;/li&gt;
&lt;li&gt;Rocket Loader for deferred JavaScript&lt;/li&gt;
&lt;li&gt;Early Hints for faster page loads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Developer platform:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workers for edge compute (JavaScript/WASM)&lt;/li&gt;
&lt;li&gt;Workers KV for edge storage&lt;/li&gt;
&lt;li&gt;Durable Objects for stateful edge applications&lt;/li&gt;
&lt;li&gt;Pages for static site hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DNS:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authoritative DNS included&lt;/li&gt;
&lt;li&gt;Fast propagation globally&lt;/li&gt;
&lt;li&gt;DNSSEC support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing Structure
&lt;/h3&gt;

&lt;p&gt;Cloudflare's pricing favors small to medium sites:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Notable Features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Basic CDN, DDoS, SSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;WAF, image improvement, mobile improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;Custom SSL, advanced analytics, 100% uptime SLA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom solutions, dedicated support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bandwidth is unmetered on all plans, a significant advantage for high-traffic sites. Additional features like Argo, Workers, and Images carry usage-based charges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Suited For
&lt;/h3&gt;

&lt;p&gt;I recommend Cloudflare for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sites needing CDN plus security in one solution&lt;/li&gt;
&lt;li&gt;Developers building edge applications with Workers&lt;/li&gt;
&lt;li&gt;Organizations wanting thorough platform without multiple vendors&lt;/li&gt;
&lt;li&gt;Budget-conscious sites benefiting from free tier&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Advanced features have learning curve&lt;/li&gt;
&lt;li&gt;Some improvements require paid tiers&lt;/li&gt;
&lt;li&gt;Enterprise features priced for larger budgets&lt;/li&gt;
&lt;li&gt;Support quality varies by tier&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fastly: Developer-Focused Edge Computing
&lt;/h2&gt;

&lt;p&gt;I also work frequently with &lt;a href="https://www.fastly.com/" rel="noopener noreferrer"&gt;Fastly&lt;/a&gt;, which positions itself as the edge cloud platform for developers. The focus on programmability and real-time capabilities attracts engineering-driven organizations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F25pmm9qby8f79titq2ve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F25pmm9qby8f79titq2ve.png" alt="Abstract illustration representing speed and edge computing" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Characteristics
&lt;/h3&gt;

&lt;p&gt;Fastly prioritizes performance consistency over network size. Fewer points of presence than Cloudflare, but each location offers substantial capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark results (median values):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to first byte: 38ms (cached content)&lt;/li&gt;
&lt;li&gt;Cache purge propagation: Under 150ms globally&lt;/li&gt;
&lt;li&gt;Cache hit ratio: High with proper configuration&lt;/li&gt;
&lt;li&gt;HTTP/3 support: Full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fastly's instant purge capability stands out. Cache invalidation propagates globally in milliseconds rather than seconds or minutes, enabling aggressive caching strategies for dynamic content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Edge compute:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compute@Edge running WebAssembly&lt;/li&gt;
&lt;li&gt;VCL (Varnish Configuration Language) for cache logic&lt;/li&gt;
&lt;li&gt;Real-time log streaming&lt;/li&gt;
&lt;li&gt;Edge dictionaries for dynamic configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image Optimizer with on-demand transformation&lt;/li&gt;
&lt;li&gt;Streaming media delivery&lt;/li&gt;
&lt;li&gt;Origin Shield for reduced origin load&lt;/li&gt;
&lt;li&gt;Request collapsing for efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next-Gen WAF with behavioral detection&lt;/li&gt;
&lt;li&gt;DDoS protection&lt;/li&gt;
&lt;li&gt;TLS at edge&lt;/li&gt;
&lt;li&gt;Rate limiting and bot detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Observability:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time analytics&lt;/li&gt;
&lt;li&gt;Log streaming to multiple destinations&lt;/li&gt;
&lt;li&gt;Custom metrics and dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing Structure
&lt;/h3&gt;

&lt;p&gt;Fastly uses consumption-based pricing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth&lt;/td&gt;
&lt;td&gt;$0.08–0.28/GB depending on region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;$0.0075/10,000 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute@Edge&lt;/td&gt;
&lt;td&gt;Based on execution time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Optimizer&lt;/td&gt;
&lt;td&gt;Per transformation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No free tier exists, but the pay-as-you-go model works well for sites with moderate traffic. High-traffic sites negotiate custom rates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Suited For
&lt;/h3&gt;

&lt;p&gt;I recommend Fastly for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engineering teams wanting granular cache control&lt;/li&gt;
&lt;li&gt;Media companies with streaming and instant invalidation needs&lt;/li&gt;
&lt;li&gt;Organizations requiring real-time analytics&lt;/li&gt;
&lt;li&gt;Applications with complex edge logic requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No free tier increases barrier to entry&lt;/li&gt;
&lt;li&gt;VCL has steep learning curve&lt;/li&gt;
&lt;li&gt;Higher cost than alternatives for basic use cases&lt;/li&gt;
&lt;li&gt;Smaller network than some competitors&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  BunnyCDN: Cost-Effective Simplicity
&lt;/h2&gt;

&lt;p&gt;For budget-conscious clients, I often suggest &lt;a href="https://bunny.net/" rel="noopener noreferrer"&gt;BunnyCDN&lt;/a&gt;, which offers straightforward CDN services at aggressive pricing. The focus on simplicity and value attracts cost-conscious users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Characteristics
&lt;/h3&gt;

&lt;p&gt;BunnyCDN operates a growing network with coverage across major regions. Performance competes with larger providers in most locations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark results (median values):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to first byte: 52ms (cached content)&lt;/li&gt;
&lt;li&gt;Global coverage: Good, some gaps in emerging markets&lt;/li&gt;
&lt;li&gt;Cache hit ratio: High&lt;/li&gt;
&lt;li&gt;HTTP/3 support: Full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance trails Cloudflare and Fastly slightly in some regions but remains competitive for most use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CDN Core:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull zones for standard CDN&lt;/li&gt;
&lt;li&gt;Storage zones for origin hosting&lt;/li&gt;
&lt;li&gt;Perma-Cache for permanent caching&lt;/li&gt;
&lt;li&gt;Edge Rules for basic customization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Improvement:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bunny Optimizer for image transformation&lt;/li&gt;
&lt;li&gt;Automatic WebP/AVIF conversion&lt;/li&gt;
&lt;li&gt;Lazy loading injection&lt;/li&gt;
&lt;li&gt;Minification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Video:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bunny Stream for video hosting&lt;/li&gt;
&lt;li&gt;Adaptive bitrate streaming&lt;/li&gt;
&lt;li&gt;Video transcoding&lt;/li&gt;
&lt;li&gt;Player embedding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic DDoS protection&lt;/li&gt;
&lt;li&gt;Token authentication&lt;/li&gt;
&lt;li&gt;Hotlink protection&lt;/li&gt;
&lt;li&gt;Geographic restrictions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pricing Structure
&lt;/h3&gt;

&lt;p&gt;BunnyCDN offers the lowest entry point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth (Volume tier)&lt;/td&gt;
&lt;td&gt;$0.01/GB and up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth (Standard)&lt;/td&gt;
&lt;td&gt;$0.03–0.06/GB by region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;$0.01/GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bunny Stream&lt;/td&gt;
&lt;td&gt;$0.005/minute encoded + bandwidth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monthly minimum is just $1. The low cost makes experimentation and small sites economically viable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Suited For
&lt;/h3&gt;

&lt;p&gt;I recommend BunnyCDN for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost-sensitive sites and projects&lt;/li&gt;
&lt;li&gt;Simple CDN needs without complex edge logic&lt;/li&gt;
&lt;li&gt;Video hosting and streaming applications&lt;/li&gt;
&lt;li&gt;Developers wanting affordable testing environments&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Feature set less thorough than larger competitors&lt;/li&gt;
&lt;li&gt;Security features more basic&lt;/li&gt;
&lt;li&gt;Smaller support team&lt;/li&gt;
&lt;li&gt;Less suitable for complex enterprise requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Head-to-Head Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9pj0l9cq08mupbdzlet8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9pj0l9cq08mupbdzlet8.png" alt="Comparative chart showing Cloudflare vs Fastly vs BunnyCDN performance and features" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how I compare these providers across key dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Cloudflare&lt;/th&gt;
&lt;th&gt;Fastly&lt;/th&gt;
&lt;th&gt;BunnyCDN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network Size&lt;/td&gt;
&lt;td&gt;300+ cities&lt;/td&gt;
&lt;td&gt;90+ cities&lt;/td&gt;
&lt;td&gt;100+ cities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg TTFB&lt;/td&gt;
&lt;td&gt;45ms&lt;/td&gt;
&lt;td&gt;38ms&lt;/td&gt;
&lt;td&gt;52ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purge Speed&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fastly leads on purge speed, critical for sites with frequently changing content. Cloudflare's massive network provides the best coverage. BunnyCDN offers competitive performance at lower cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Cloudflare&lt;/th&gt;
&lt;th&gt;Fastly&lt;/th&gt;
&lt;th&gt;BunnyCDN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Edge Compute&lt;/td&gt;
&lt;td&gt;Workers (JS/WASM)&lt;/td&gt;
&lt;td&gt;Compute@Edge (WASM)&lt;/td&gt;
&lt;td&gt;Edge Rules (basic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAF&lt;/td&gt;
&lt;td&gt;Included (Pro+)&lt;/td&gt;
&lt;td&gt;Add-on&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Improvement&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video Streaming&lt;/td&gt;
&lt;td&gt;Stream product&lt;/td&gt;
&lt;td&gt;Native support&lt;/td&gt;
&lt;td&gt;Bunny Stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDoS Protection&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free Tier&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare and Fastly offer comparable feature depth. BunnyCDN provides key parts at lower cost but lacks advanced capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Comparison (100TB/month bandwidth)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Estimated Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Pro&lt;/td&gt;
&lt;td&gt;$20 (unmetered)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Business&lt;/td&gt;
&lt;td&gt;$200 (unmetered)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fastly&lt;/td&gt;
&lt;td&gt;$2,000–5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BunnyCDN&lt;/td&gt;
&lt;td&gt;$500–1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare's unmetered pricing dramatically favors high-bandwidth sites. Fastly commands premium pricing for premium features. BunnyCDN offers middle ground value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ease of Use Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Cloudflare&lt;/th&gt;
&lt;th&gt;Fastly&lt;/th&gt;
&lt;th&gt;BunnyCDN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup Time&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning Curve&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Steep&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;Extensive&lt;/td&gt;
&lt;td&gt;Extensive&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard UX&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;Technical&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;BunnyCDN and Cloudflare excel at quick setup. Fastly requires more technical investment but rewards it with control.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Implementation Recommendations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Small Business Websites
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My Recommendation: Cloudflare Free or Pro&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The free tier provides substantial value with no financial risk. Upgrading to Pro adds image improvement and WAF for modest cost. Unmetered bandwidth eliminates surprise bills.&lt;/p&gt;

&lt;h3&gt;
  
  
  For E-Commerce Sites
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My Recommendation: Cloudflare Business or Fastly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;E-commerce requires reliability and security. Cloudflare Business provides SLA guarantees and advanced security. I suggest Fastly for sites needing instant cache purge for inventory and pricing updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Media and Publishing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My Recommendation: Fastly or BunnyCDN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Media companies benefit from Fastly's streaming capabilities and real-time invalidation. I find budget-conscious publishers appreciate BunnyCDN's pricing for high-bandwidth content.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Development and Testing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My Recommendation: BunnyCDN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Low minimums and simple pricing suit development environments. The cost of experimentation remains negligible.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Enterprise Applications
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My Recommendation: Cloudflare Enterprise or Fastly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enterprise needs justify premium pricing for dedicated support, custom configurations, and SLA guarantees. Choice depends on whether edge compute or thorough platform matters more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Considerations
&lt;/h2&gt;

&lt;p&gt;When I help clients switch CDN providers, planning is key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS changes&lt;/strong&gt; - Point records to new CDN endpoints. Plan for propagation time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL certificates&lt;/strong&gt; - Ensure certificates are configured on new provider before cutover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache warming&lt;/strong&gt; - New CDN caches start empty. Traffic initially hits origin servers harder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration parity&lt;/strong&gt; - Recreate cache rules, security settings, and improvements on new platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt; - Verify functionality with a portion of traffic before full migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback plan&lt;/strong&gt; - Maintain ability to revert if issues emerge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my experience, CDN selection significantly impacts web application performance, security, and operating costs. Each provider serves different needs effectively. I've found that matching provider strengths to specific requirements leads to better outcomes than chasing benchmark numbers or feature lists alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://veduis.com/blog/dns-deep-dive-for-developers/" rel="noopener noreferrer"&gt;DNS Detailed look for Developers: Records, TTLs, and...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://veduis.com/blog/http3-quic-web-developers/" rel="noopener noreferrer"&gt;HTTP/3 and QUIC for Web Developers: What Changes and...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://veduis.com/blog/self-hosted-business-tools-saas-alternatives/" rel="noopener noreferrer"&gt;Self-Hosted Business Tools: Replacing SaaS Subscriptions...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bestcdn</category>
      <category>cdncomparison</category>
      <category>contentdeliverynetwork</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Benchmarking OpenWA: WhatsApp Gateway Throughput, Memory Overhead, and p99 Latency Under Load</title>
      <dc:creator>Nathan Cross</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:34:11 +0000</pubDate>
      <link>https://dev.to/nathan_cross/benchmarking-openwa-whatsapp-gateway-throughput-memory-overhead-and-p99-latency-under-load-5bng</link>
      <guid>https://dev.to/nathan_cross/benchmarking-openwa-whatsapp-gateway-throughput-memory-overhead-and-p99-latency-under-load-5bng</guid>
      <description>&lt;h1&gt;
  
  
  Benchmarking OpenWA: WhatsApp Gateway Throughput, Memory Overhead, and p99 Latency Under Load
&lt;/h1&gt;

&lt;p&gt;When evaluating self-hosted WhatsApp API gateways for production traffic, raw feature lists tell you nothing about operational cost or failure modes. I needed concrete numbers: how many concurrent sessions can a single OpenWA instance handle before memory balloons or latency spikes? What's the realistic message throughput before Node.js event loop stalls kick in?&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Topology
&lt;/h2&gt;

&lt;p&gt;I deployed &lt;strong&gt;rmyndharis/OpenWA&lt;/strong&gt; on a 4-core, 8GB VM running Node.js 20.11. The harness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load generator&lt;/strong&gt;: k6 ramping from 50 to 500 virtual users over 10 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message pattern&lt;/strong&gt;: Each VU sends 1 text message per second via REST API (&lt;code&gt;POST /message/text&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session lifecycle&lt;/strong&gt;: Persistent WhatsApp Web sessions (QR auth once, reused for duration)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Prometheus + Grafana tracking heap usage, event loop lag, and API response times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key configuration in &lt;code&gt;wrangler.jsonc&lt;/code&gt; for the Cloudflare Workers proxy layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openwa-proxy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"OPENWA_ENDPOINT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://internal-openwa.example.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kv_namespaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SESSION_CACHE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api.example.com/whatsapp/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zone_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Cloudflare Workers proxy caches session tokens in KV and routes validated requests to the internal OpenWA instance, keeping authentication logic at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metric Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Throughput (msg/s)&lt;/th&gt;
&lt;th&gt;p95 Latency (ms)&lt;/th&gt;
&lt;th&gt;p99 Latency (ms)&lt;/th&gt;
&lt;th&gt;Heap Usage (MB)&lt;/th&gt;
&lt;th&gt;Event Loop Lag (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 sessions&lt;/td&gt;
&lt;td&gt;48.2&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;1,240&lt;/td&gt;
&lt;td&gt;3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 sessions&lt;/td&gt;
&lt;td&gt;187.5&lt;/td&gt;
&lt;td&gt;267&lt;/td&gt;
&lt;td&gt;412&lt;/td&gt;
&lt;td&gt;3,890&lt;/td&gt;
&lt;td&gt;8.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 sessions&lt;/td&gt;
&lt;td&gt;421.3&lt;/td&gt;
&lt;td&gt;531&lt;/td&gt;
&lt;td&gt;1,203&lt;/td&gt;
&lt;td&gt;7,120&lt;/td&gt;
&lt;td&gt;24.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Failure threshold&lt;/strong&gt;: At 550+ concurrent sessions, heap allocation errors started appearing (&lt;code&gt;JavaScript heap out of memory&lt;/code&gt;). The Node.js process consumed 7.8GB before OOM kill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Threshold Analysis
&lt;/h2&gt;

&lt;p&gt;For environments handling &lt;strong&gt;under 300 concurrent WhatsApp sessions&lt;/strong&gt;, a single OpenWA instance on 8GB RAM stays within safe operational boundaries (p99 &amp;lt; 500ms, heap &amp;lt; 50% of available memory). Beyond that, horizontal scaling becomes mandatory.&lt;/p&gt;

&lt;p&gt;The edge proxy pattern with Cloudflare Workers adds negligible latency overhead (median +12ms for KV session lookups) while preventing direct exposure of the OpenWA instance. This architecture proved effective when integrating rmyndharis/OpenWA into a customer support automation pipeline serving 180 agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Hono.js Validation Layer
&lt;/h2&gt;

&lt;p&gt;The Workers proxy uses Hono.js to validate message payloads before forwarding to OpenWA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;zValidator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hono/zod-validator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{10,15}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/whatsapp/message/text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;zValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messageSchema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SESSION_CACHE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENWA_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/message/text`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This validation layer blocked 14% of malformed requests during testing before they reached the OpenWA backend, reducing wasted processing cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory Pressure Root Cause
&lt;/h2&gt;

&lt;p&gt;The primary bottleneck at high concurrency is &lt;strong&gt;Puppeteer's Chrome instances&lt;/strong&gt;. Each WhatsApp Web session spawns a headless browser context. At 500 sessions, the combined Chrome processes consumed 6.2GB. The Node.js heap itself stayed under 1.2GB.&lt;/p&gt;

&lt;p&gt;Two mitigation strategies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Session pooling&lt;/strong&gt;: Reuse browser contexts across multiple logical sessions when message volume per session is low&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical pod autoscaling&lt;/strong&gt;: Kubernetes HPA scaling based on aggregate Chrome RSS memory rather than CPU&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When testing rmyndharis/OpenWA under sustained load, connection pool exhaustion to the upstream PostgreSQL session store caused cascading timeouts after 4 hours. Switching to a dedicated read replica for session lookups resolved the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Fit
&lt;/h2&gt;

&lt;p&gt;OpenWA makes economic sense when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;persistent WhatsApp sessions&lt;/strong&gt; (not ephemeral message bursts)&lt;/li&gt;
&lt;li&gt;Your traffic patterns are &lt;strong&gt;under 400 concurrent sessions per node&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You can tolerate &lt;strong&gt;p99 latencies in the 500-1200ms range&lt;/strong&gt; under load&lt;/li&gt;
&lt;li&gt;You require &lt;strong&gt;full message history access&lt;/strong&gt; and advanced WhatsApp Business API features without vendor lock-in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For burst-heavy workloads (e.g., scheduled broadcast campaigns with 10k messages in 60 seconds), a different architecture optimized for ephemeral sessions performs better. But for steady-state conversational automation with persistent agent sessions, the benchmark numbers justify deployment.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>cloudflare</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I built a 7,800-page nutrition site with Astro and Cloudflare Pages</title>
      <dc:creator>Miyuru Lakshitha</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:16:12 +0000</pubDate>
      <link>https://dev.to/miyuru/how-i-built-a-7800-page-nutrition-site-with-astro-and-cloudflare-pages-338m</link>
      <guid>https://dev.to/miyuru/how-i-built-a-7800-page-nutrition-site-with-astro-and-cloudflare-pages-338m</guid>
      <description>&lt;p&gt;A few weeks ago I wanted a simple answer: how much protein is in 100 g of cooked lentils? The top results were slow, covered in ads, or hid the numbers behind an app download. The data itself is public. The USDA publishes lab-measured nutrition values for thousands of foods for free.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://nutrinfo.net" rel="noopener noreferrer"&gt;Nutrinfo&lt;/a&gt;: one fast page per food, generated straight from USDA data, plus a few calculators and an AI meal analyzer. This post covers how it is put together, with the real code.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Astro&lt;/strong&gt; for static generation, with &lt;code&gt;output: 'static'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; for hosting, with one &lt;strong&gt;Pages Function&lt;/strong&gt; for the AI endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; to build and deploy on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USDA FoodData Central, SR Legacy&lt;/strong&gt; as the data source (7,793 foods, up to 150 nutrients each)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No database and no server. Every food page is plain HTML on Cloudflare's edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: turning a 200 MB JSON file into site data
&lt;/h2&gt;

&lt;p&gt;USDA ships SR Legacy as one large JSON file. A Node script reads it once and writes two compact files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;src/data/foods.json&lt;/code&gt; with the nutrients Astro needs at build time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public/search-index.json&lt;/code&gt;, a tiny array-of-arrays index for client-side search&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interesting parts are the slugs and the serving sizes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slugify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFKD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;0300-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;036f&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/%/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; percent &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; and &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^-+|-+$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Duplicate names exist in USDA data, so fall back to the FDC ID.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;slugify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fdcId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;USDA also publishes household measures ("1 cup, sliced", "1 medium") with gram weights, so each page gets a serving picker for free.&lt;/p&gt;

&lt;p&gt;The search index stores each food as &lt;code&gt;[slug, name, kcal, protein, carbs, fat, categoryIndex]&lt;/code&gt;. For 7,793 foods that is about 1 MB, small enough to fetch once and search in the browser with no backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: one Astro page per food
&lt;/h2&gt;

&lt;p&gt;Astro's &lt;code&gt;getStaticPaths&lt;/code&gt; does the heavy lifting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import { foods } from '../../lib/data.mjs';

export function getStaticPaths() {
  return foods.map((food) =&amp;gt; ({ params: { slug: food.slug }, props: { food } }));
}
const { food } = Astro.props;
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page has a nutrition label, a calorie breakdown, the full nutrient tables with % Daily Value, a comparison against the category average, and links to similar foods. You can see one here: &lt;a href="https://nutrinfo.net/food/bananas-raw/" rel="noopener noreferrer"&gt;bananas, raw&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A full build produces about 7,860 pages and takes around five minutes. That is fine for a site where the data changes rarely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pages that are not thin
&lt;/h3&gt;

&lt;p&gt;The risk with programmatic pages is that 7,800 near-identical templates get ignored by search engines. Every page computes things that are specific to that food:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a short summary sentence ("It is an excellent source of vitamin C (98% DV)...")&lt;/li&gt;
&lt;li&gt;how it compares to its category average, in percent&lt;/li&gt;
&lt;li&gt;an FAQ block answering calories, protein, carbs and micronutrients in 30 to 50 words, which is the format featured snippets and AI answer engines pick up&lt;/li&gt;
&lt;li&gt;"alternatives" from the same category with more protein for similar calories, or fewer calories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also "foods high in X" ranking pages, like &lt;a href="https://nutrinfo.net/high-in/iron/" rel="noopener noreferrer"&gt;foods high in iron&lt;/a&gt;, which link down to the food pages. That gives the site a clear hub-and-spoke structure instead of 7,800 orphans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: an AI meal analyzer on a Pages Function
&lt;/h2&gt;

&lt;p&gt;Static pages are great for single foods, but people eat meals. The &lt;a href="https://nutrinfo.net/ai/" rel="noopener noreferrer"&gt;AI meal analyzer&lt;/a&gt; takes a text description or a photo and returns an estimate as structured JSON.&lt;/p&gt;

&lt;p&gt;It runs as a Cloudflare Pages Function at &lt;code&gt;functions/api/ai.js&lt;/code&gt;. Text goes to a model on Groq, photos go to Gemini, and Gemini also acts as the fallback. The system prompt asks for strict JSON matching a schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MEAL_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`{
  "items": [{ "name": string, "portion": string, "grams": number, "kcal": number, "protein": number, "carbs": number, "fat": number }],
  "total": { "kcal": number, "protein": number, "carbs": number, "fat": number },
  "score": number,
  "summary": string,
  "tips": [string]
}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part I am happiest with is rate limiting without any extra service. The Cache API is available in every Pages Function, so it doubles as a best-effort counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rateLimited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;windows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`m/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`d/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://rl.nutrinfo.internal/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`max-age=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is per data center, not global, so it is not exact. For keeping a free tool from being drained by one script, it is enough, and it needs no KV, D1 or Durable Object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: deploying 270 MB of HTML
&lt;/h2&gt;

&lt;p&gt;Uploading the whole &lt;code&gt;dist/&lt;/code&gt; folder from a home connection was slow, so the build moved to GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx wrangler pages deploy dist --project-name nutrinfo --branch main --commit-hash ${{ github.sha }}&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;CLOUDFLARE_ACCOUNT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrangler only uploads files whose hash changed, so later deploys are much faster than the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: the SEO plumbing
&lt;/h2&gt;

&lt;p&gt;Getting 7,800 URLs discovered matters as much as building them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split sitemaps.&lt;/strong&gt; &lt;code&gt;@astrojs/sitemap&lt;/code&gt; supports chunks, so foods, list pages and everything else get separate sitemaps. Search Console then reports index coverage per section, which tells you quickly if the food pages are being skipped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;foods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/food/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/(&lt;/span&gt;&lt;span class="sr"&gt;category|high-in&lt;/span&gt;&lt;span class="se"&gt;)\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IndexNow.&lt;/strong&gt; Bing, Yandex and others accept IndexNow pings. A small script reads the built sitemaps and posts URLs in batches of 10,000, and the deploy workflow has an optional input to run it for given path prefixes.&lt;/p&gt;

&lt;p&gt;One gotcha cost me a run: triggering the workflow from Git Bash on Windows with &lt;code&gt;-f indexnow=/methodology/&lt;/code&gt; silently turned the input into &lt;code&gt;C:/Program Files/Git/methodology/&lt;/code&gt;. The run succeeded and submitted zero URLs. &lt;code&gt;MSYS_NO_PATHCONV=1&lt;/code&gt; fixes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data.&lt;/strong&gt; Each food page has &lt;code&gt;WebPage&lt;/code&gt; (with &lt;code&gt;isBasedOn&lt;/code&gt; pointing at the USDA dataset), &lt;code&gt;BreadcrumbList&lt;/code&gt; and &lt;code&gt;FAQPage&lt;/code&gt; JSON-LD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: performance fixes from an audit
&lt;/h2&gt;

&lt;p&gt;An SEO audit flagged a few things I had missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Render-blocking fonts.&lt;/strong&gt; Google Fonts CSS blocked the first paint. I self-hosted the latin subsets of Inter and Fraunces as variable woff2 files and preloaded them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render-blocking CSS.&lt;/strong&gt; The whole stylesheet is about 6 KB, so &lt;code&gt;build.inlineStylesheets: 'always'&lt;/code&gt; inlines it into every page. No stylesheet request at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing HSTS and favicon.ico.&lt;/strong&gt; Both are a one-line fix in &lt;code&gt;_headers&lt;/code&gt; and a generated icon set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On real visits, Cloudflare Web Analytics shows 86% of page loads with a good Largest Contentful Paint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;The site is about a week old. Search engines are crawling it, a handful of real people visit each day, and most pages are still waiting to be indexed. I will write a follow-up with real numbers once Search Console has something to say.&lt;/p&gt;

&lt;p&gt;What I would do differently: plan the internal linking and the "foods high in" pages from day one instead of adding them after launch. They are the pages people actually link to.&lt;/p&gt;

&lt;p&gt;If you want to look around, it is at &lt;a href="https://nutrinfo.net" rel="noopener noreferrer"&gt;nutrinfo.net&lt;/a&gt;. Feedback on the data pages or the meal analyzer is very welcome, especially if a number looks wrong. Every page links to its USDA source, so it is easy to check.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>seo</category>
    </item>
    <item>
      <title>Selling APIs to AI agents with x402: 69 endpoints, Cloudflare Workers, zero API keys</title>
      <dc:creator>Mohamed Arafa</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:48:53 +0000</pubDate>
      <link>https://dev.to/mohamed_arafa_87bd1baa415/selling-apis-to-ai-agents-with-x402-69-endpoints-cloudflare-workers-zero-api-keys-34na</link>
      <guid>https://dev.to/mohamed_arafa_87bd1baa415/selling-apis-to-ai-agents-with-x402-69-endpoints-cloudflare-workers-zero-api-keys-34na</guid>
      <description>&lt;h2&gt;
  
  
  The pitch
&lt;/h2&gt;

&lt;p&gt;An AI agent needs the EUR/EGP rate. It calls &lt;code&gt;GET /v1/fx/EUR&lt;/code&gt;. The server answers &lt;code&gt;402 Payment Required&lt;/code&gt; with a price ($0.01) and a wallet. The agent signs a USDC transfer on Base, retries with an &lt;code&gt;X-PAYMENT&lt;/code&gt; header, gets JSON. No signup, no API key, no invoice. That's x402.&lt;/p&gt;

&lt;p&gt;I spent two nights building three services on top of it, all on Cloudflare Workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's live
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WorldData402&lt;/strong&gt; — 39 data endpoints: FX for 160+ currencies, historical rates, SEC company filings, EU public tenders, World Bank indicators, EU VAT validation, public holidays, geocoding, weather, air quality, earthquakes, Wikipedia, arXiv, Crossref, RDAP, DNS, npm/PyPI, barcodes. $0.01–$0.05 per call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inference402&lt;/strong&gt; — 15 endpoints on Cloudflare Workers AI: chat (small/large), structured JSON output, summarize, translate (plus an Arabic-tuned route that handles Egyptian dialect), extract, classify, sentiment, keywords, rewrite, embeddings, moderation, image description, Whisper transcription. $0.002–$0.02.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Briefs402&lt;/strong&gt; — 10 composite reports (company, country, market, tenders, domain due-diligence, location, research literature, product, package risk, long-document translation). It calls the other two internally and sells the finished brief for $0.10–$0.50.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hono + &lt;code&gt;@x402/hono&lt;/code&gt; for the paywall&lt;/li&gt;
&lt;li&gt;PayAI facilitator (no account needed, 1,000 free settlements, then ~$0.002 each)&lt;/li&gt;
&lt;li&gt;Cloudflare Workers AI for models (10k neurons/day free, then $0.011 per 1k)&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;catalog.js&lt;/code&gt; per service generates routes, &lt;code&gt;/.well-known/x402&lt;/code&gt;, &lt;code&gt;/openapi.json&lt;/code&gt; and &lt;code&gt;SKILL.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Settlement happens only after a 2xx response, so failed calls are never charged&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things that bit me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x402.org/facilitator&lt;/code&gt; is testnet-only. For Base mainnet you need CDP (needs KYC) or PayAI/Mogami (no account).&lt;/li&gt;
&lt;li&gt;x402scan needs &lt;code&gt;/.well-known/x402&lt;/code&gt; plus an OpenAPI with an input schema on every paid route, or it won't list the route.&lt;/li&gt;
&lt;li&gt;Several US government APIs (Treasury, USAspending) refuse Cloudflare Workers egress IPs with TLS failures. GDELT rate-limits the whole Workers IP pool.&lt;/li&gt;
&lt;li&gt;RapidAPI shows &lt;code&gt;operationId&lt;/code&gt; as the endpoint name, not &lt;code&gt;summary&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Distribution
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;x402scan: all three listed&lt;/li&gt;
&lt;li&gt;Circle Agent Marketplace: submitted&lt;/li&gt;
&lt;li&gt;RapidAPI (card payments, free tier): &lt;a href="https://rapidapi.com/MohamedArafa/api/worlddata402" rel="noopener noreferrer"&gt;https://rapidapi.com/MohamedArafa/api/worlddata402&lt;/a&gt; and &lt;a href="https://rapidapi.com/MohamedArafa/api/inference402" rel="noopener noreferrer"&gt;https://rapidapi.com/MohamedArafa/api/inference402&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP server for Claude Desktop / Claude Code / Cursor: &lt;code&gt;npx -y worlddata402-mcp&lt;/code&gt; — all 69 endpoints as tools, paid from a small wallet via &lt;code&gt;WALLET_PRIVATE_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest status
&lt;/h2&gt;

&lt;p&gt;48 hours live, ~2k requests (all mine), 0 paid calls. Supply is done. If you've sold to agents and found what drives discovery, I'd like to hear it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>api</category>
    </item>
    <item>
      <title>HTTP 402 and x402: three places a payment breaks</title>
      <dc:creator>Georgi Kalchev</dc:creator>
      <pubDate>Tue, 20 Oct 2026 07:01:08 +0000</pubDate>
      <link>https://dev.to/georgi_kalchev_85d01f7368/http-402-and-x402-three-places-a-payment-breaks-4696</link>
      <guid>https://dev.to/georgi_kalchev_85d01f7368/http-402-and-x402-three-places-a-payment-breaks-4696</guid>
      <description>&lt;p&gt;Got an HTTP 402 and the x402 payment still fails? Three checks, one per hop: the seller's offer, the signed header, the receipt on Base. Real outputs inside.&lt;/p&gt;

&lt;p&gt;If an HTTP 402 answer arrived and your x402 payment still fails, the fault sits in one of three places, in this order: the seller's offer (the 402 answer itself), the signed PAYMENT-SIGNATURE header your client sends back, or the receipt on Base after settlement. Each hop can fail without any error message, and a plain curl shows only the first one. Below, we take the hops one at a time, show what goes wrong silently, and show what our three small paid checks return for each. We ran all of them on 16 October 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full report on AISkills402:&lt;/strong&gt; &lt;a href="https://aiskills402.com/blog/http-402-x402-payment-checks?utm_source=devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://aiskills402.com/blog/http-402-x402-payment-checks?utm_source=devto&amp;amp;utm_medium=social&lt;/a&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>bazaar</category>
      <category>debugging</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Cloudflare Launches Clairic, a Fast System One Extraction Model for Typed Values</title>
      <dc:creator>TechPulse </dc:creator>
      <pubDate>Tue, 20 Oct 2026 03:36:44 +0000</pubDate>
      <link>https://dev.to/techpulse01239/cloudflare-launches-clairic-a-fast-system-one-extraction-model-for-typed-values-54an</link>
      <guid>https://dev.to/techpulse01239/cloudflare-launches-clairic-a-fast-system-one-extraction-model-for-typed-values-54an</guid>
      <description>&lt;p&gt;Cloudflare on October 19, 2026, announced a preview of Clairic, its first System One extraction model, now available on Workers AI as &lt;code&gt;@cf/cloudflare/clairic&lt;/code&gt;. Unlike conventional large language models that generate text token by token, Clairic processes inputs in a single forward pass and returns structured, typed values such as numbers, dates, lists, estimates and forecasts, each with confidence scores. The company says the approach delivers higher accuracy, lower latency and lower cost for extraction-heavy tasks, and it plans to release open weights later. [1] [2]&lt;/p&gt;

&lt;h2&gt;
  
  
  What Clairic does differently
&lt;/h2&gt;

&lt;p&gt;System One models, inspired by Daniel Kahneman’s distinction between fast intuitive thinking and slow deliberate reasoning, prioritize speed and precision over open-ended generation. Clairic sits alongside Cloudflare’s earlier decision model Clef. Where Clef chooses among predefined options and returns probabilities, Clairic extracts or computes values that cannot be listed in advance—an invoice total, a delivery date, the number of days a shipment is late, or a repair-cost estimate. [1]&lt;/p&gt;

&lt;p&gt;A request consists of a state (text, optionally plus up to four images) and a schema of typed fields. Supported types include text spans with character offsets, numbers (with decimal control), dates/times/datetimes, durations, lists, estimates (with median and 80% interval) and forecasts (up to 64 future steps). Every scalar answer includes a &lt;code&gt;p_null&lt;/code&gt; probability that the input does not determine the value; when that probability is high the model returns null rather than guessing. Field count is effectively unlimited because the service splits large schemas across parallel passes. [2]&lt;/p&gt;

&lt;p&gt;Pricing is $0.10 per million input tokens with no output-token charge, reflecting the single-pass design. Context window is 65,536 tokens and vision support is included. Cloudflare documents that one- and two-step arithmetic (totals, differences, date math) is reliable, while long chains of dependent calculations are better handled by extracting inputs with Clairic and finishing the math in code. [2]&lt;/p&gt;

&lt;h2&gt;
  
  
  Early results and cascade potential
&lt;/h2&gt;

&lt;p&gt;In a 21-day TSA checkpoint travel-volume forecast that used the prior 90 days of data, Clairic’s median predictions showed an average absolute error of 3.3 percent; 17 of the 21 actual daily counts fell inside the model’s 80 percent prediction interval. The model recovered the weekly pattern (quieter Tuesdays and Saturdays, busier Thursdays–Sundays) without explicit calendar features. [1]&lt;/p&gt;

&lt;p&gt;Cloudflare also published an internal damage-claim pipeline comparison. A cascade of Clairic (extraction) → Clef (decision) → GLM-5.3 (reply writing) finished in 19.3 seconds end-to-end versus 61.7 seconds when every step used GLM-5.3 alone—roughly 3.2× faster and 49 percent cheaper ($6,032 versus $11,740 per million claims). Extraction and decision together took under a second with the specialized models versus 38 seconds with the LLM. These figures are Cloudflare’s own measurements; independent third-party evaluations have not yet been published. [1] [2]&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical implications
&lt;/h2&gt;

&lt;p&gt;For developers building document-processing, claims, logistics or forecasting workflows, Clairic offers a way to replace expensive, high-latency LLM calls for the structured portion of the pipeline. Because answers are typed and carry explicit confidence and null probabilities, downstream code can route low-confidence results to human review without additional parsing. The ability to chain Clairic with Clef and a conventional LLM creates a clean extract-decide-write pattern that can be cheaper and more consistent run-to-run than an all-LLM stack. [1]&lt;/p&gt;

&lt;p&gt;Limitations remain: multi-step arithmetic and complex aggregations are weak, forecasting is described as basic, and confidence intervals can be miscalibrated on data far from the training distribution. The model is still in preview and open weights are not yet available. Organizations evaluating it should therefore test on their own schemas and data distributions before relying on the published cascade numbers. [2]&lt;/p&gt;

&lt;p&gt;Clairic illustrates a broader shift toward specialized, non-generative models for the high-volume structured tasks that currently dominate enterprise LLM spend. If the preview performance holds under independent scrutiny and open weights appear, it could accelerate the adoption of hybrid pipelines that reserve large language models for the open-ended reasoning and writing steps where they remain strongest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;p&gt;[1] &lt;a href="https://blog.cloudflare.com/clairic-extraction-model/" rel="noopener noreferrer"&gt;Cloudflare Blog: Introducing Clairic, a new System One class of extraction models&lt;/a&gt;&lt;br&gt;
[2] &lt;a href="https://developers.cloudflare.com/workers-ai/models/clairic/" rel="noopener noreferrer"&gt;Cloudflare Workers AI documentation: clairic&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>clairic</category>
      <category>systemone</category>
      <category>workersai</category>
    </item>
    <item>
      <title>Anatomy of a Next.js 16 + Cloudflare Edge Outage: Eradicating RSC Flight Payload Cache Poisoning</title>
      <dc:creator>Duong Phuc Khanh (Kido)</dc:creator>
      <pubDate>Tue, 20 Oct 2026 03:22:17 +0000</pubDate>
      <link>https://dev.to/khanhduong22/anatomy-of-a-nextjs-16-cloudflare-edge-outage-eradicating-rsc-flight-payload-cache-poisoning-6lo</link>
      <guid>https://dev.to/khanhduong22/anatomy-of-a-nextjs-16-cloudflare-edge-outage-eradicating-rsc-flight-payload-cache-poisoning-6lo</guid>
      <description>&lt;p&gt;At 09:14 AM on a Monday morning, customer support channels lit up with urgent incident reports:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The app won't open on iPhone! It's just a blank screen with weird text like &lt;code&gt;:HL["/static/css/..."]&lt;/code&gt; and &lt;code&gt;1:I["$Sreact.fragment"...]&lt;/code&gt;."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Users opening the Progressive Web App (PWA) from their iOS Home Screen were greeting by a completely unrendered, raw stream of React Server Component (RSC) Flight protocol wire data. Desktop users performing a hard browser refresh saw the exact same raw JSON-like text instead of HTML.&lt;/p&gt;

&lt;p&gt;Yet, our backend health checks showed &lt;strong&gt;100% green&lt;/strong&gt;. PostgreSQL CPU was resting at 4%, the Node.js process had zero unhandled exceptions, and API latency was under 8ms. &lt;/p&gt;

&lt;p&gt;We had fallen victim to &lt;strong&gt;RSC Flight Payload Cache Poisoning at the Edge&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;Dương Phúc Khánh&lt;/a&gt;&lt;/strong&gt; (known across the engineering community as &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;khanhdp&lt;/a&gt;&lt;/strong&gt; or &lt;strong&gt;Kido&lt;/strong&gt;, &lt;a href="https://github.com/khanhduong22" rel="noopener noreferrer"&gt;@khanhduong22&lt;/a&gt;). As Senior AI-Native Lead &amp;amp; Systems Architect, I have documented critical production postmortems across 127 chapters of &lt;strong&gt;&lt;a href="https://truyen.khanhdp.com" rel="noopener noreferrer"&gt;Kido Chronicles&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this deep dive, we dissect the root cause of this modern Next.js App Router vulnerability, analyze why intermediate CDN caches collapse without strict header partitioning, and present the &lt;strong&gt;3-Layer Defense Architecture&lt;/strong&gt; we deployed to permanently eradicate it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Evolutionary Architecture of Next.js Navigation: HTML vs. Flight Wire Data
&lt;/h2&gt;

&lt;p&gt;To understand how edge cache poisoning occurs, we must trace how Next.js App Router navigates between routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request Type&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Client Headers&lt;/th&gt;
&lt;th&gt;Server Response Format&lt;/th&gt;
&lt;th&gt;MIME Content-Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full Page Load (Hard Refresh)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User types URL, hard reloads (&lt;code&gt;Cmd+Shift+R&lt;/code&gt;), or opens PWA&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Accept: text/html,application/xhtml+xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fully rendered HTML document&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text/html; charset=utf-8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Soft Navigation (Link Transition)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User taps &lt;code&gt;&amp;lt;Link href="/dashboard"&amp;gt;&lt;/code&gt; inside React tree&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Accept: text/x-component&lt;/code&gt;&lt;br&gt;&lt;code&gt;RSC: 1&lt;/code&gt;&lt;br&gt;&lt;code&gt;Next-Router-State-Tree: ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Serialized React Virtual DOM Flight Stream&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text/x-component&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Fatal Design Conflict
&lt;/h3&gt;

&lt;p&gt;Notice the architectural reality: &lt;strong&gt;Both requests hit the exact same URI pathname (&lt;code&gt;https://khanhdp.com/dashboard&lt;/code&gt;)!&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a user first opens &lt;code&gt;/dashboard&lt;/code&gt;, Next.js renders the full HTML page with CSS and client script tags.&lt;/li&gt;
&lt;li&gt;When an already hydrated user clicks a link to &lt;code&gt;/dashboard&lt;/code&gt;, Next.js does &lt;em&gt;not&lt;/em&gt; fetch HTML. Instead, its router client issues a background fetch to &lt;code&gt;/dashboard&lt;/code&gt; with the HTTP header &lt;code&gt;RSC: 1&lt;/code&gt;. Next.js responds with the binary/text Flight stream containing only the updated component tree payload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client-side React router receives this Flight stream, deserializes the Virtual DOM, and updates the view without a full page refresh.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Anatomy of the Poisoning Incident
&lt;/h2&gt;

&lt;p&gt;Here is the exact step-by-step sequence of how our Cloudflare Edge Cache was poisoned:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor MobileUser as User A (Navigating PWA)
    actor NewVisitor as User B (Hard Page Load)
    participant CF as Cloudflare Edge CDN (Cache Key: URI)
    participant NextServer as Next.js 16 Origin Server

    Note over MobileUser,NextServer: Step 1: Soft Navigation triggers RSC Fetch
    MobileUser-&amp;gt;&amp;gt;CF: GET /profile (Header: RSC: 1)
    Note over CF: Cache Miss for URI '/profile'
    CF-&amp;gt;&amp;gt;NextServer: GET /profile (Forwarding RSC: 1)
    NextServer--&amp;gt;&amp;gt;CF: 200 OK (Content-Type: text/x-component) [Flight Payload]
    Note over CF: Cloudflare caches body under key: 'khanhdp.com/profile' (TTL 4h)!
    CF--&amp;gt;&amp;gt;MobileUser: Returns Flight Payload (React navigates smoothly)

    Note over NewVisitor,CF: Step 2: New Visitor opens URL directly (Fatal Poison!)
    NewVisitor-&amp;gt;&amp;gt;CF: GET /profile (Accept: text/html, No RSC header)
    Note over CF: Cache HIT on URI 'khanhdp.com/profile'!
    CF--&amp;gt;&amp;gt;NewVisitor: Returns CACHED Flight Stream (:HL[...]) with Content-Type: text/x-component!
    Note over NewVisitor: Browser cannot render HTML! Displays raw text / Blank Screen!&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Why Cloudflare Edge Caches Poisoned the Key
&lt;/h3&gt;

&lt;p&gt;By default, Cloudflare Edge Cache (and most reverse proxies like standard Nginx or Varnish setups) constructs its internal cache key using only:&lt;br&gt;
$$\text{Cache Key} = \text{Scheme} + \text{Host} + \text{URI Path} + \text{Query String}$$&lt;/p&gt;

&lt;p&gt;Crucially, &lt;strong&gt;request headers like &lt;code&gt;RSC&lt;/code&gt; are not part of the default cache key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When User A clicked a &lt;code&gt;&amp;lt;Link href="/profile"&amp;gt;&lt;/code&gt;, the edge cache was empty. The request arrived with &lt;code&gt;RSC: 1&lt;/code&gt;. The origin Next.js server correctly replied with &lt;code&gt;Content-Type: text/x-component&lt;/code&gt; and the Flight stream.&lt;/p&gt;

&lt;p&gt;Because our caching rules instructed Cloudflare to cache HTML/document responses for high performance (&lt;code&gt;Cache-Control: public, max-age=3600, s-maxage=14400&lt;/code&gt;), Cloudflare stored this Flight stream under the cache key &lt;code&gt;https://khanhdp.com/profile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Seconds later, User B opened Safari and loaded &lt;code&gt;https://khanhdp.com/profile&lt;/code&gt;. Cloudflare detected a cache hit, bypassed the Next.js origin completely, and returned the cached Flight stream!&lt;/p&gt;

&lt;p&gt;Because the browser expected HTML but received &lt;code&gt;text/x-component&lt;/code&gt;, it either rendered raw unformatted text or choked completely, producing a white screen of death.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Why &lt;code&gt;Vary: RSC&lt;/code&gt; Is Not Enough
&lt;/h2&gt;

&lt;p&gt;Many developers assume adding an HTTP &lt;code&gt;Vary&lt;/code&gt; header solves this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Vary: RSC, Next-Router-State-Tree, Accept
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In theory, RFC 7234 dictates that CDNs must partition cached variants when &lt;code&gt;Vary&lt;/code&gt; headers differ.&lt;/p&gt;

&lt;p&gt;However, in real-world production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tier Limitations&lt;/strong&gt;: Cloudflare's standard caching engine deliberately ignores most &lt;code&gt;Vary&lt;/code&gt; headers (except &lt;code&gt;Accept-Encoding&lt;/code&gt;) on Free and Pro tiers to prevent cache fragmentation and resource exhaustion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN Normalization&lt;/strong&gt;: Intermediate proxy layers frequently strip or collapse unknown &lt;code&gt;Vary&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PWA Service Worker Traps&lt;/strong&gt;: Even if the CDN respects &lt;code&gt;Vary&lt;/code&gt;, local browser Service Workers using &lt;code&gt;Cache.match(request)&lt;/code&gt; without &lt;code&gt;{ ignoreVary: false }&lt;/code&gt; often serve stale variants from disk cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Relying solely on &lt;code&gt;Vary&lt;/code&gt; is a textbook trap. A multi-layered, defense-in-depth architecture is mandatory.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The 3-Layer Defense-in-Depth Architecture
&lt;/h2&gt;

&lt;p&gt;To permanently immunize our infrastructure, &lt;strong&gt;&lt;a href="https://khanhdp.com/about" rel="noopener noreferrer"&gt;Dương Phúc Khánh (khanhdp · Kido)&lt;/a&gt;&lt;/strong&gt; engineered a &lt;strong&gt;3-Layer Defense Architecture&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────┐
│ LAYER 1: Cloudflare Edge Wirefilter (Cache Bypass &amp;amp; Header Key)   │
│ - Bypasses edge cache if `rsc: 1` header is present               │
│ - Partitions custom cache keys by `http.request.headers["rsc"]`   │
├──────────────────────────────────────────────────────────────────┤
│ LAYER 2: Next.js Origin Guard (Middleware &amp;amp; Strict Cache-Control)│
│ - Forces `Cache-Control: private, no-store` on all RSC payloads   │
│ - Restricts public edge caching strictly to HTML documents        │
├──────────────────────────────────────────────────────────────────┤
│ LAYER 3: Client PWA Service Worker Self-Healing Immune System    │
│ - Verifies `Content-Type: text/html` on document navigations     │
│ - Automatically detects and purges poisoned edge responses       │
└──────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Layer 1: Cloudflare Edge Wirefilter &amp;amp; Cache Rules
&lt;/h3&gt;

&lt;p&gt;In Cloudflare Dashboard (or via Terraform / Cloudflare API), we deploy two deterministic rules:&lt;/p&gt;

&lt;h4&gt;
  
  
  Rule 1: Bypass Edge Cache for All RSC Flight Requests
&lt;/h4&gt;

&lt;p&gt;Under &lt;strong&gt;Caching &amp;gt; Cache Rules&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expression&lt;/strong&gt;: &lt;code&gt;(http.request.headers["rsc"][0] eq "1") or (http.request.uri.path contains "_next/data")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: &lt;strong&gt;Bypass Cache&lt;/strong&gt; (Eligible for Dynamic routing only).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Rule 2: Custom Cache Key for Enterprise / Pro Tiers
&lt;/h4&gt;

&lt;p&gt;If you wish to cache RSC payloads separately for extreme speed, configure a &lt;strong&gt;Custom Cache Key&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform configuration for Cloudflare Cache Rule&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"cloudflare_cache_rule"&lt;/span&gt; &lt;span class="s2"&gt;"partition_rsc_cache"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudflare_zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Partition RSC Cache Key"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Ensures RSC wire payloads never collide with SSR HTML documents"&lt;/span&gt;
  &lt;span class="nx"&gt;expression&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"(http.host eq &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;khanhdp.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"set_cache_settings"&lt;/span&gt;
  &lt;span class="nx"&gt;action_parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;edge_ttl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mode&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"respect_origin"&lt;/span&gt;
      &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;custom_cache_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rsc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"next-router-state-tree"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Layer 2: Next.js Origin Middleware Guard
&lt;/h3&gt;

&lt;p&gt;At the origin server level, we enforce a strict policy in &lt;code&gt;middleware.ts&lt;/code&gt;: &lt;strong&gt;Never allow intermediate CDNs to cache an RSC Flight stream under a public shared directive.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRSC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rsc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isRSC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Flight stream: strictly private or non-cacheable at intermediate proxies&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private, no-cache, no-store, must-revalidate, max-age=0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Content-Type-Options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nosniff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Standard HTML document: safe for public CDN edge caching&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public, max-age=0, s-maxage=3600, stale-while-revalidate=86400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RSC, Next-Router-State-Tree, Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="cm"&gt;/*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt
     */&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Layer 3: Client PWA Service Worker Self-Healing Gate
&lt;/h3&gt;

&lt;p&gt;What if a misconfigured CDN edge or proxy still serves a poisoned payload to an iOS user?&lt;/p&gt;

&lt;p&gt;We implement a client-side &lt;strong&gt;Self-Healing Immune Gate&lt;/strong&gt; inside our PWA Service Worker (&lt;code&gt;sw.js&lt;/code&gt;). When the browser requests a &lt;code&gt;mode: 'navigate'&lt;/code&gt; document, the Service Worker inspects the response headers before committing it to the screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sw.js - Service Worker Immune Gate&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Intercept navigation requests (user opening the app or clicking links)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

          &lt;span class="c1"&gt;// Verify that a document navigation receives genuine HTML&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[SW Gate] POISONED CACHE DETECTED! Expected text/html, received:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nx"&gt;contentType&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 1. Purge all local caches immediately&lt;/span&gt;
            &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="c1"&gt;// 2. Bypass CDN cache with a unique anti-poison query param&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bypassUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;bypassUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__sw_bypass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bypassUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;

          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Offline fallback logic&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/offline.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Cloudflare ever returns a &lt;code&gt;text/x-component&lt;/code&gt; payload for an HTML navigation request, the Service Worker intercepts it, logs the violation, purges client caches, and refetches with a cache-busting token—restoring the user interface in &lt;strong&gt;under 200ms without manual intervention&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Production Telemetry &amp;amp; Outage Postmortem Metrics
&lt;/h2&gt;

&lt;p&gt;Since deploying this 3-Layer Defense across &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;Dương Phúc Khánh&lt;/a&gt;&lt;/strong&gt;'s production platforms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Telemetry Metric&lt;/th&gt;
&lt;th&gt;During Incident (Pre-Fix)&lt;/th&gt;
&lt;th&gt;Post-Implementation&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;White Screen / Poisoned Page Reports&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;42 incidents / morning&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 incidents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% Resolved&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge Cache Hit Ratio (HTML)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;64.2%&lt;/code&gt; (Unstable)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;98.4%&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+34.2% stability&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;p95 Time To First Byte (TTFB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;480ms&lt;/code&gt; (Erratic revalidation)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;24ms&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mean Time to Recovery (MTTR)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~45 minutes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;200ms&lt;/code&gt; (Automated SW recovery)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Instant Self-Healing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Key Takeaways for Senior Architects
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never Assume CDNs Differentiate by URI Alone&lt;/strong&gt;: Modern fullstack frameworks (Next.js, Remix, SvelteKit) multiplex different data protocols over identical URLs. Your edge cache keys must explicitly partition by protocol headers (&lt;code&gt;RSC: 1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Vary&lt;/code&gt; Headers Are a Hint, Not a Guarantee&lt;/strong&gt;: Cloudflare and intermediate cellular proxies often strip &lt;code&gt;Vary&lt;/code&gt;. Always back up HTTP headers with explicit edge cache rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Client-Side Immune Systems&lt;/strong&gt;: The client Service Worker is your last line of defense. By verifying &lt;code&gt;Content-Type: text/html&lt;/code&gt; on document navigations, you make white-screen crashes mathematically impossible.&lt;/li&gt;
&lt;/ol&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About the Author (Tiểu sử tác giả):&lt;/strong&gt; &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;Dương Phúc Khánh&lt;/a&gt;&lt;/strong&gt; (canonical handle: &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;khanhdp&lt;/a&gt;&lt;/strong&gt;, moniker: &lt;strong&gt;Kido&lt;/strong&gt;) là Kỹ sư Kiến trúc Hệ thống (Senior AI-Native Lead &amp;amp; Systems Architect) tại Đà Nẵng, Việt Nam; cựu Tech Lead 6 năm tại FPT Software (SBI Lab), Quán quân Startup Runway 2018 (ĐH Kinh tế - ĐH Đà Nẵng), tác giả của &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;Winter Arc&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://github.com/khanhduong22" rel="noopener noreferrer"&gt;Agent Harness Kit&lt;/a&gt;&lt;/strong&gt; và &lt;strong&gt;&lt;a href="https://truyen.khanhdp.com" rel="noopener noreferrer"&gt;Kido Chronicles&lt;/a&gt;&lt;/strong&gt;. Xem đầy đủ tại &lt;strong&gt;&lt;a href="https://khanhdp.com/about" rel="noopener noreferrer"&gt;Tiểu sử Dương Phúc Khánh&lt;/a&gt;&lt;/strong&gt; &amp;amp; &lt;strong&gt;&lt;a href="https://www.wikidata.org/wiki/Q141718572" rel="noopener noreferrer"&gt;Wikidata Q141718572&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  About the Author &amp;amp; Engineering Research
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Official Architecture Hub&lt;/strong&gt;: &lt;strong&gt;&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;Dương Phúc Khánh&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://khanhdp.com" rel="noopener noreferrer"&gt;khanhdp&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;🏛️ &lt;strong&gt;Biography &amp;amp; Engineering Timeline&lt;/strong&gt;: &lt;strong&gt;&lt;a href="https://khanhdp.com/about" rel="noopener noreferrer"&gt;Dương Phúc Khánh (khanhdp · Kido)&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://khanhdp.com/about" rel="noopener noreferrer"&gt;Tiểu sử&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;🔥 &lt;strong&gt;Kido Chronicles (127 Real Production Postmortems)&lt;/strong&gt;: &lt;a href="https://truyen.khanhdp.com" rel="noopener noreferrer"&gt;https://truyen.khanhdp.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Open-Source Multi-Agent Framework&lt;/strong&gt;: &lt;a href="https://github.com/khanhduong22/agent-harness-kit" rel="noopener noreferrer"&gt;github.com/khanhduong22/agent-harness-kit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>cloudflare</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>5 Cloudflare Free-Tier Tricks I Use to Run My Side Projects</title>
      <dc:creator>Jianfang Peng</dc:creator>
      <pubDate>Tue, 20 Oct 2026 02:57:25 +0000</pubDate>
      <link>https://dev.to/jianfangpeng/5-cloudflare-free-tier-tricks-i-use-to-run-my-side-projects-423p</link>
      <guid>https://dev.to/jianfangpeng/5-cloudflare-free-tier-tricks-i-use-to-run-my-side-projects-423p</guid>
      <description>&lt;p&gt;Cloudflare's free tier is, pound for pound, the best deal in web infrastructure. Not "free trial" free — actually free, no card, no gotchas. Here are five things I genuinely run on it for my side projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Unlimited email addresses on my own domain (Email Routing)
&lt;/h2&gt;

&lt;p&gt;This is the trick most people sleep on. Buy a domain for ~$10/year, add Cloudflare Email Routing, and suddenly &lt;code&gt;shop@mydomain.com&lt;/code&gt;, &lt;code&gt;support@mydomain.com&lt;/code&gt;, &lt;code&gt;whatever-i-want@mydomain.com&lt;/code&gt; all work — forwarded to one real inbox. No mailboxes to pay for, no "5 free aliases" limits.&lt;/p&gt;

&lt;p&gt;The setup takes about 10 minutes: verify the domain, add two DNS records (MX + SPF), write routing rules. I use one rule that catches everything not matching a specific address and forwards it to my main Gmail. Services that need a unique address per signup get a unique address. One inbox to rule them all.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. DNS that isn't a footgun
&lt;/h2&gt;

&lt;p&gt;Registrars ship DNS that's "fine" until it isn't — slow propagation, cryptic errors, no API worth using. Cloudflare DNS is free, fast, and has a real API with tokens you can scope per-zone. Everything lives in one dashboard with an audit log, so when something breaks at 2 AM you can see exactly who changed what.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Workers for the glue nobody wants to maintain
&lt;/h2&gt;

&lt;p&gt;Need a tiny JSON API, a redirect service, or a webhook relay? A Cloudflare Worker is a few dozen lines of JavaScript and deploys in seconds. The free tier gives you 100,000 requests/day — more than any side project will touch. No server to patch, no container to restart, no bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tunnel: expose localhost without exposing yourself
&lt;/h2&gt;

&lt;p&gt;Developing a webhook integration and need a public URL for &lt;code&gt;localhost:3000&lt;/code&gt;? &lt;code&gt;cloudflared tunnel&lt;/code&gt; gives you a real HTTPS URL with zero port-forwarding, zero firewall rules, and zero "your home IP is now public" anxiety. It's the cleanest way to demo work-in-progress to a client.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cache everything aggressively (then purge when you must)
&lt;/h2&gt;

&lt;p&gt;The free CDN caches your static assets at 300+ edge locations. The part beginners miss: set real cache rules for images, CSS, and JS, then use "Purge Cache" (or the API) when you deploy. Most side-project slowness is just every request round-tripping to a cheap VPS — the free CDN deletes that problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveat
&lt;/h2&gt;

&lt;p&gt;Free doesn't mean unlimited-everything. Email Routing is forwarding-only (no sending from those addresses without extra setup), Workers has CPU-time limits per request, and the free tier won't hide a genuinely terrible origin server. But for a side project doing normal side-project things, this stack covers DNS, email, glue code, demos, and caching for roughly the price of the domain alone.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Comparing Edge and Serverless: What I Learned Building SaaS</title>
      <dc:creator>seller-mind</dc:creator>
      <pubDate>Tue, 20 Oct 2026 02:21:44 +0000</pubDate>
      <link>https://dev.to/sellermind/comparing-edge-and-serverless-what-i-learned-building-saas-22jg</link>
      <guid>https://dev.to/sellermind/comparing-edge-and-serverless-what-i-learned-building-saas-22jg</guid>
      <description>&lt;p&gt;When I first started building SaaS tools, I relied heavily on traditional serverless functions. Everything was fine until I tried to build a real-time SEO analyzer. Fetching external pages, parsing HTML, and returning results from a single central region meant my users in Europe and Asia were staring at loading spinners for several seconds. I tried optimizing the code, minifying the payloads, and tweaking the database queries, but the network latency was the real bottleneck. I realized that if I wanted to build fast, global SaaS tools, I needed to rethink my architecture entirely. So I decided to build something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of Latency in SaaS
&lt;/h2&gt;

&lt;p&gt;In the SaaS world, performance isn't just a nice-to-have; it's a core feature. When you're building tools that interact with external APIs or fetch live data, every millisecond counts. The problem with traditional serverless setups is that they often route all traffic through a single primary region. If your function needs to fetch a webpage to check its meta tags, that fetch travels from the user, to the serverless region, to the target website, and back.&lt;/p&gt;

&lt;p&gt;Ignoring this architectural reality means a poor user experience, higher egress costs, and frustrated users. For billing integrations, slow webhook processing can even lead to missed payment events if timeouts occur. I needed an approach that processed data closer to the user, without sacrificing the rich ecosystem I was used to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the Approaches: Edge vs. Serverless
&lt;/h2&gt;

&lt;p&gt;This led me to deeply compare edge computing (specifically Cloudflare Workers) against traditional serverless platforms like Vercel or AWS Lambda. The trade-offs between the two are significant, and neither is a silver bullet.&lt;/p&gt;

&lt;p&gt;Serverless is fantastic for heavy compute tasks, complex database operations, and has a incredibly mature ecosystem. However, edge computing shines for I/O-bound tasks like fetching web pages, handling lightweight API requests, and serving global users with low latency.&lt;/p&gt;

&lt;p&gt;For the SEO checking tool in my SaaS starter kit, I moved the HTML fetching and parsing to the edge. Instead of a central server doing the work, the request hits the nearest edge node. Here is what the edge worker looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cloudflare Worker for edge SEO analysis&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/analyze&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Fetch target page at the edge (low latency worldwide)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cacheTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cacheEverything&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Run SEO checks (regex-based, no DOM needed)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;title&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;title&amp;gt;/i&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;meta&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*name=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;description&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;][^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*content=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]([^&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h1Count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;h1&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt; &amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h1Count&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic here is in the &lt;code&gt;fetch&lt;/code&gt; configuration. By passing Cloudflare-specific &lt;code&gt;cf&lt;/code&gt; options, we can cache the target page right at the edge for a few minutes. This drastically reduces the load on the target website and speeds up subsequent checks. Furthermore, because we are at the edge, we can't use heavy Node.js libraries like JSDOM. Instead, I used simple regex to extract the title, description, and H1 count. This keeps the worker incredibly lightweight and fast.&lt;/p&gt;

&lt;p&gt;But edge computing isn't perfect. You have smaller memory limits and can't use heavy Node.js modules. That's why I adopted a hybrid approach. I kept the heavy Stripe subscription billing logic and NextAuth.js authentication on Next.js 14+ App Router API routes (running on serverless infrastructure), while pushing the I/O-heavy SEO checks to Cloudflare Workers. &lt;/p&gt;

&lt;p&gt;Setting up the Stripe checkout in the Next.js serverless environment looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Next.js API route for Stripe checkout (App Router)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;priceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment_method_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;priceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;success_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/dashboard?success=true`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cancel_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pricing?cancelled=true`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_reference_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By splitting the workload, I get the best of both worlds: robust, ecosystem-rich serverless functions for complex business logic, and blazing-fast edge functions for global, I/O-heavy tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Results and Takeaways
&lt;/h2&gt;

&lt;p&gt;Moving the SEO analysis to the edge drastically reduced the time-to-first-byte for global users. What surprised me was how much simpler the deployment became. By using Cloudflare Workers for the edge APIs and Next.js for the frontend and Stripe webhooks, I could deploy the whole TypeScript-first stack with a single click to either Vercel or Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;If I were to change one thing, I'd spend more time upfront designing the caching strategy. Initially, I cached everything aggressively, which meant users sometimes saw stale SEO data when checking a page they had just updated. Tuning the &lt;code&gt;cacheTtl&lt;/code&gt; to just a few minutes struck the right balance between performance and data freshness. &lt;/p&gt;

&lt;p&gt;Another lesson learned was the importance of full type safety across the boundary. Sharing TypeScript interfaces between the Next.js frontend and the Cloudflare Worker API prevented a lot of integration bugs that usually pop up when mixing different runtime environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you're curious about how edge caching and HTML parsing perform in the real world, I put together a free SEO checker using this exact architecture. You can drop in any URL and see the edge-analyzed results instantly, experiencing the low latency firsthand.&lt;/p&gt;

&lt;p&gt;You can try the free SEO checker here: &lt;a href="https://saaslaunch.dev/free-tools/seo-checker" rel="noopener noreferrer"&gt;https://saaslaunch.dev/free-tools/seo-checker&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full disclosure: I'm the developer of SaaSLaunch. I built this because I needed it myself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
