<?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>AI crawler user agents are self-reported: 468 real fetches, 991 fake ones</title>
      <dc:creator>Aulvem</dc:creator>
      <pubDate>Sat, 22 Aug 2026 03:31:26 +0000</pubDate>
      <link>https://dev.to/aulvem/ai-crawler-user-agents-are-self-reported-468-real-fetches-991-fake-ones-bgo</link>
      <guid>https://dev.to/aulvem/ai-crawler-user-agents-are-self-reported-468-real-fetches-991-fake-ones-bgo</guid>
      <description>&lt;p&gt;I used to watch AI crawler traffic on my site as a table grouped by User-Agent: so many requests from ChatGPT-User, so many from GPTBot. Numbers going up meant the AI systems were picking the site up.&lt;/p&gt;

&lt;p&gt;Then I re-cut eight days of logs by verification result. Requests that actually fetched an article: 468. Requests probing for &lt;code&gt;.env&lt;/code&gt; and friends: 991. Of everything calling itself GPTBot, Cloudflare could verify 13% as OpenAI.&lt;/p&gt;

&lt;p&gt;Here is how to separate the impersonators on a Cloudflare free plan, and what the numbers looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  A User-Agent is a claim, not evidence
&lt;/h2&gt;

&lt;p&gt;Writing &lt;code&gt;GPTBot/1.2&lt;/code&gt; into a header costs nothing, so a table grouped by UA is a list of claims.&lt;/p&gt;

&lt;p&gt;Behind Cloudflare there are two things to check those claims against:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;verifiedBotCategory&lt;/code&gt;&lt;/strong&gt; — Cloudflare's reverse-DNS verification result. &lt;strong&gt;Empty string means unverified&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether the requested path exists in your sitemap&lt;/strong&gt; — separates reading an article from crawl bookkeeping such as robots.txt&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What survives both filters is "verified bots fetching real pages", and that is the only number worth reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query (works on a free zone)
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;userAgent&lt;/code&gt; and &lt;code&gt;verifiedBotCategory&lt;/code&gt; to the dimensions of &lt;code&gt;httpRequestsAdaptiveGroups&lt;/code&gt;. &lt;code&gt;clientAsn&lt;/code&gt; and &lt;code&gt;botClass&lt;/code&gt; require a paid plan; &lt;code&gt;verifiedBotCategory&lt;/code&gt; does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;QUERY_BY_UA_VERIFIED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
query ($zoneTag: String!, $since: Time!, $until: Time!) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      httpRequestsAdaptiveGroups(
        limit: 5000
        filter: { datetime_geq: $since, datetime_leq: $until }
        orderBy: [count_DESC]
      ) {
        count
        dimensions { userAgent verifiedBotCategory }
      }
    }
  }
}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two constraints to plan around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One request covers at most one day.&lt;/strong&gt; Chunk the range in the caller&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention is short&lt;/strong&gt; (roughly a week at daily granularity). You cannot re-derive the past, so write a snapshot to disk on every run
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# split the range into one-day windows and concatenate the rows
&lt;/span&gt;    &lt;span class="n"&gt;all_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;
    &lt;span class="n"&gt;one_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&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="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;win_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;one_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zoneTag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zone_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;since&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;until&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;win_end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;all_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rows_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exit_on_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    [skipped] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;win_end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# past retention
&lt;/span&gt;        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win_end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;all_rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One UA string, two rows
&lt;/h2&gt;

&lt;p&gt;That single extra dimension is enough to split claim from reality. Eight days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claimed UA&lt;/th&gt;
&lt;th&gt;Verified&lt;/th&gt;
&lt;th&gt;Unverified&lt;/th&gt;
&lt;th&gt;Verified share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT-User&lt;/td&gt;
&lt;td&gt;211 (AI Assistant)&lt;/td&gt;
&lt;td&gt;336&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazonbot&lt;/td&gt;
&lt;td&gt;135 (AI Crawler)&lt;/td&gt;
&lt;td&gt;489&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;133 (AI Crawler)&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAI-SearchBot&lt;/td&gt;
&lt;td&gt;61 (Search Engine Crawler)&lt;/td&gt;
&lt;td&gt;132&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;19 (AI Crawler)&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;meta-externalagent&lt;/td&gt;
&lt;td&gt;209 (AI Crawler)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applebot&lt;/td&gt;
&lt;td&gt;56 (AI Search)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerplexityBot&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;144&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity-User&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;311&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of 547 requests presenting as &lt;code&gt;ChatGPT-User&lt;/code&gt;, 211 came from an address that traced back to OpenAI.&lt;/p&gt;

&lt;p&gt;Only two agents came through clean — &lt;code&gt;meta-externalagent&lt;/code&gt; and &lt;code&gt;Applebot&lt;/code&gt;, 100% verified with zero impersonation. Those are the only rows whose claimed totals are usable as-is. All 455 Perplexity-branded requests were unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  One of the agents cannot exist
&lt;/h2&gt;

&lt;p&gt;123 requests claimed &lt;code&gt;Google-Extended&lt;/code&gt;. Verified share 0%, and 70 of them hit credential-scanning paths.&lt;/p&gt;

&lt;p&gt;No inference required. Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers" rel="noopener noreferrer"&gt;crawler documentation&lt;/a&gt; states that &lt;code&gt;Google-Extended&lt;/code&gt; has no separate HTTP request user agent string: crawling happens under the existing Google user agents, and the token exists purely to be addressed in robots.txt for AI-training control.&lt;/p&gt;

&lt;p&gt;So every request presenting that UA is, by definition, not Google. The same trick works for any operator that publishes IP ranges — OpenAI ships &lt;a href="https://openai.com/gptbot.json" rel="noopener noreferrer"&gt;gptbot.json&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify the path, not just the client
&lt;/h2&gt;

&lt;p&gt;Verification alone isn't enough: a verified bot fetching robots.txt has read nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SCAN_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wp-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.svn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ssh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secrets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service_account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actuator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api/auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phpinfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.bak&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id_rsa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.npmrc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.htpasswd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OPS_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/robots.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sitemap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/llms.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/favicon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/rss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/feed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/.well-known/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ASSET_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/_astro/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/images/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/assets/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/fonts/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/cdn-cgi/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/_image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sitemap_paths&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# content = a real page was consumed / ops = crawl bookkeeping
&lt;/span&gt;    &lt;span class="c1"&gt;# asset = static file / scan = credential probing / other = path does not exist
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SCAN_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;low&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="n"&gt;OPS_PREFIXES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;low&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="n"&gt;ASSET_PREFIXES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sitemap_paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# cannot assert existence, so cannot call it content
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sitemap_paths&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the sitemap as the source of truth for existence is the part that holds up. Deciding from the response status looks easier, but redirects and paths that answer 200 without being real pages both leak in. The set of paths you declared public is a cleaner definition of "a page of mine".&lt;/p&gt;

&lt;p&gt;Keep the &lt;code&gt;unknown&lt;/code&gt; branch too. Fold it into &lt;code&gt;content&lt;/code&gt; and your numbers spike on any day the sitemap fetch fails, with nothing in the output to explain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result: 468 content fetches against 991 scans
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claimed UA&lt;/th&gt;
&lt;th&gt;content&lt;/th&gt;
&lt;th&gt;ops&lt;/th&gt;
&lt;th&gt;scan&lt;/th&gt;
&lt;th&gt;total&lt;/th&gt;
&lt;th&gt;verified&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT-User&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;181&lt;/td&gt;
&lt;td&gt;547&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;meta-externalagent&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;209&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazonbot&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;284&lt;/td&gt;
&lt;td&gt;624&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applebot&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAI-SearchBot&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerplexityBot&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;144&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;129&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;259&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google-Extended&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity-User&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;td&gt;311&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;content&lt;/code&gt; totals 468, &lt;code&gt;scan&lt;/code&gt; totals 991. By status code, 403s came to 957 against 705 served with 200.&lt;/p&gt;

&lt;p&gt;The ClaudeBot row is worth pulling apart: of 133 verified requests, 6 fetched articles and 129 fetched robots.txt and similar. "ClaudeBot sent 259 requests" and "six articles were read" are the same data.&lt;/p&gt;

&lt;p&gt;Keep the &lt;strong&gt;verified share as a column&lt;/strong&gt; in whatever you output. When a series' share collapses, that is your signal to stop reading it as a metric for that snapshot.&lt;/p&gt;




&lt;p&gt;The week-over-week comparison (content 610 → 468 while scan went 357 → 991), why I cannot tell a genuine drop in interest apart from impersonation being reclassified, and the point where WAF blocks overtook served requests are all on Aulvem → &lt;a href="https://aulvem.com/blog/2026-08-22-ai-crawler-ua-verification/" rel="noopener noreferrer"&gt;Aulvem | AI crawler user agents are self-reported: 468 real fetches, 991 fake ones&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>python</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>I Built a Crypto Market Intelligence App with React Native, Supabase &amp; Cloudflare Workers AI</title>
      <dc:creator>Alligator Peach</dc:creator>
      <pubDate>Sat, 22 Aug 2026 00:59:05 +0000</pubDate>
      <link>https://dev.to/alligator_peach_developer/i-built-a-crypto-market-intelligence-app-with-react-native-supabase-cloudflare-workers-ai-4j0e</link>
      <guid>https://dev.to/alligator_peach_developer/i-built-a-crypto-market-intelligence-app-with-react-native-supabase-cloudflare-workers-ai-4j0e</guid>
      <description>&lt;h2&gt;
  
  
  The problem: alert fatigue
&lt;/h2&gt;

&lt;p&gt;Keeping up with crypto meant juggling five different apps — a price tracker, a scanner, news feeds, sentiment charts, and a stack of alert apps. And even then I kept missing the moves that mattered.&lt;/p&gt;

&lt;p&gt;The existing tools fell into two camps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too noisy&lt;/strong&gt; — thousands of alerts, most of them irrelevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too shallow&lt;/strong&gt; — pretty charts without any real analysis behind them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;SignalWatch&lt;/strong&gt;: a mobile app that turns overwhelming market data into a clear, prioritized signal board. It scans 100+ coins, understands plain-English questions, tracks market sentiment, and only alerts you about assets that actually pass the bar.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;React Native (Expo SDK 54), TypeScript, React Navigation 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth &amp;amp; Sync&lt;/td&gt;
&lt;td&gt;Supabase (Google OAuth + email/password)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analysis engine&lt;/td&gt;
&lt;td&gt;Node/Express on Cloud Run — scheduled every 15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data ingestion&lt;/td&gt;
&lt;td&gt;Cloudflare Workers (CoinMarketCap + Binance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI semantic scanner&lt;/td&gt;
&lt;td&gt;Cloudflare Workers AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;FCM via a Supabase Edge Function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market data&lt;/td&gt;
&lt;td&gt;Binance &amp;amp; CoinMarketCap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│         React Native App (Expo)             │
│  Dashboard · Scanner · Signal · Calendar    │
└───────┬──────────────────┬──────────────────┘
        │ Supabase queries │ POST /api/ai-query (Bearer JWT)
        ▼                  ▼
  ┌────────────┐    ┌──────────────────┐
  │  Supabase  │    │ Cloudflare Worker│
  │  (Postgres)│    │  Workers AI      │
  └─────▲──────┘    └────────▲─────────┘
        │                    │
┌───────┴────────┐   ┌───────┴────────┐
│  Cloud Run     │   │  Cloudflare    │
│  (analysis,    │   │  Worker        │
│   every 15min) │   │  (CMC ingest)  │
└───────▲────────┘   └───────▲────────┘
        │                    │
        └──── Binance + CoinMarketCap ────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two services feed a shared Supabase database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run backend&lt;/strong&gt; — technical analysis (RSI, MACD, EMA, Bollinger Bands, SuperTrend) across futures &amp;amp; spot markets, plus economic calendar events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Worker&lt;/strong&gt; — CoinMarketCap ingestion (top 100, trending, fear &amp;amp; greed, news) and the AI query endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app itself is architected with thin screens over a central hook (&lt;code&gt;useDashboard&lt;/code&gt;) that acts as the single source of truth, with a clean service layer for all data access.&lt;/p&gt;




&lt;h2&gt;
  
  
  The feature I'm most proud of: the AI semantic scanner
&lt;/h2&gt;

&lt;p&gt;Users can ask questions in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"show me coins with RSI &amp;lt; 30 and volume spike &amp;gt; 100%"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The query goes to a Cloudflare Worker, which detects intent, pulls real data from Supabase, and lets Workers AI compose a structured, data-backed answer. No hardcoded results, no fake confidence scores.&lt;/p&gt;

&lt;p&gt;One subtle but critical detail: the app authenticates to the worker with the user's Supabase access token (&lt;code&gt;Bearer&lt;/code&gt;), and the worker validates it against the Supabase JWKS. No secrets are ever embedded in the app binary.&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;// src/services/ai.ts — get a valid token, then call the worker&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getValidAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userData&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;userData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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;refreshed&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&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;refreshed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refreshData&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="nx"&gt;refreshError&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refreshSession&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;refreshError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;refreshData&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;access_token&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;refreshData&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;access_token&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;null&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;
  
  
  ⚠️ The ES256 vs RS256 gotcha
&lt;/h3&gt;

&lt;p&gt;This project taught me a lesson: &lt;strong&gt;not all Supabase JWTs are RS256&lt;/strong&gt;. This particular project signs tokens with &lt;strong&gt;ES256 (EC P-256)&lt;/strong&gt;. My worker's JWT verifier originally only handled RS256, so &lt;em&gt;every&lt;/em&gt; valid token was rejected with a confusing 401 — "Please sign in" — even though the user was clearly signed in.&lt;/p&gt;

&lt;p&gt;The fix was to support both ES256 and RS256 in the worker's verification path, plus logging exactly &lt;em&gt;why&lt;/em&gt; verification failed (much easier to debug than a generic 401).&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons learned along the way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. External sites will block your cloud IPs
&lt;/h3&gt;

&lt;p&gt;ForexFactory (for the economic calendar's news feed) returns &lt;strong&gt;HTTP 403 to Cloud Run datacenter IPs&lt;/strong&gt;. The calendar HTML page works fine, but the news scrape doesn't. My solution: a graceful &lt;strong&gt;fallback&lt;/strong&gt; — if the &lt;code&gt;forex_news&lt;/code&gt; table is empty or the request fails, the app falls back to CoinMarketCap news (&lt;code&gt;news_insights&lt;/code&gt;), which is populated by the Cloudflare Worker. The section is never empty, and users can't tell the difference.&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;// src/services/marketCalendar.ts — fallback to CMC news when forex news fails&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;fetchForexNews&lt;/span&gt; &lt;span class="o"&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;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ForexNewsRow&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;data&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;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forex_news&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&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;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published_at&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;ascending&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&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;forexNews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ForexNewsRow&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;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;forexNews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;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;return&lt;/span&gt; &lt;span class="nx"&gt;forexNews&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...fallback to news_insights (CoinMarketCap via worker)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Data freshness is a feature
&lt;/h3&gt;

&lt;p&gt;A pipeline that ingests every 15 minutes across multiple timeframes (15m / 1h / 4h / 1d) means the "now" is actually &lt;em&gt;now&lt;/em&gt;. That matters enormously in fast-moving markets — and it's a real competitive differentiator, not a buzzword.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Alert fatigue is the real enemy
&lt;/h3&gt;

&lt;p&gt;Most tools push &lt;em&gt;volume&lt;/em&gt;, not &lt;em&gt;signal&lt;/em&gt;. SignalWatch bakes in quality filters so users only get notified about assets that actually pass the bar. It sounds obvious, but it's the thing that makes the product feel calm instead of chaotic.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep the app architecture boring
&lt;/h3&gt;

&lt;p&gt;Thin screens → one central hook → a service layer → Supabase/worker. It kept a solo developer sane, made the codebase easy to grow, and made the light/dark theme + EN/ID localization trivial to add.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎯 AI scanner you can talk to (Cloudflare Workers AI over live data)&lt;/li&gt;
&lt;li&gt;📊 Multi-timeframe technical analysis on 100+ coins&lt;/li&gt;
&lt;li&gt;🌍 Global sentiment (Fear &amp;amp; Greed, bullish ratio, market score)&lt;/li&gt;
&lt;li&gt;🔔 Quality-filtered push notifications (no spam)&lt;/li&gt;
&lt;li&gt;⭐ Synced watchlist, 📅 economic calendar, 📰 impact-labeled news&lt;/li&gt;
&lt;li&gt;🌗 Light/dark + EN/ID, fully customizable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently available on &lt;strong&gt;Google Play&lt;/strong&gt; with all Pro features free. iOS is next (the codebase is Expo, so the path is clear).&lt;/p&gt;

&lt;p&gt;If you've hit similar walls with external APIs blocking cloud IPs, or JWT algorithms that don't match the docs — I'd love to hear how you solved it. Feedback and feature ideas are very welcome. 👇&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.builtwithme.signalwatch" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>supabase</category>
      <category>cloudflare</category>
      <category>ai</category>
    </item>
    <item>
      <title>Cloudflare Bot Preference Sync will write your robots.txt from 21 August 2026 - and it ignores your custom rules</title>
      <dc:creator>Manu Shukla</dc:creator>
      <pubDate>Fri, 21 Aug 2026 23:42:54 +0000</pubDate>
      <link>https://dev.to/mr_manushukla/cloudflare-bot-preference-sync-will-write-your-robotstxt-from-21-august-2026-and-it-ignores-your-1053</link>
      <guid>https://dev.to/mr_manushukla/cloudflare-bot-preference-sync-will-write-your-robotstxt-from-21-august-2026-and-it-ignores-your-1053</guid>
      <description>&lt;h1&gt;
  
  
  Cloudflare Bot Preference Sync will write your robots.txt from 21 August 2026 - and it ignores your custom rules
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Summary.&lt;/strong&gt; Cloudflare published "Say it once: introducing Bot Preference SynC" at 23:19 UTC on 21 August 2026. The feature generates or rewrites your &lt;code&gt;robots.txt&lt;/code&gt; from the Search, Agent and Training settings you already hold in the zone dashboard, and it is promised to every plan tier from Free to Enterprise "in the coming week". Three details in that post matter more than the headline. It works only on category-wide policy, so it will not read your per-crawler custom rules. It is on by default for all new customers. And as of this writing the documentation page it needs, at &lt;code&gt;/ai-crawl-control/features/managed-robots-txt/&lt;/code&gt;, returns a 404, while the page that does exist - the legacy &lt;code&gt;robots.txt&lt;/code&gt; setting doc, last updated 3 August 2026 - still describes a different mechanism that hard-writes &lt;code&gt;Disallow: /&lt;/code&gt; for 8 named crawlers. If you run a site behind Cloudflare with a hand-maintained &lt;code&gt;robots.txt&lt;/code&gt;, the sequencing here decides whether your file says what you think it says on 15 September 2026, when Cloudflare's new AI traffic defaults take effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cloudflare actually announced
&lt;/h2&gt;

&lt;p&gt;The mechanism is narrow and worth stating exactly. Bot Preference Sync does not introduce new blocking. It takes the AI bot policy you have already configured at zone level - Allow, Block on pages that serve ads, or Block everywhere for Search and Agent, plus Disallow for Training - and reflects that policy as text inside &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your origin already serves a &lt;code&gt;robots.txt&lt;/code&gt;, Cloudflare prepends its block ahead of your content rather than replacing it, wrapped in two comment markers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# BEGIN Cloudflare Bot Preference Sync

User-agent: TrainingBot1
User-agent: TrainingBot2
User-agent: TrainingBot3
User-agent: MixedUseBot-Extended
Disallow: /

...

# END Cloudflare Bot Preference Sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That example is Cloudflare's own, shortened and anonymised in the post. The real user-agent list is pulled from &lt;a href="https://developers.cloudflare.com/bots/botbase/" rel="noopener noreferrer"&gt;BotBase&lt;/a&gt;, Cloudflare's bot database launched on 1 July 2026, and Cloudflare says it will "periodically update the list of bots that is added to robots.txt when you choose to Block or Disallow a given category". You do not pin that list. Cloudflare does, and it changes underneath you.&lt;/p&gt;

&lt;p&gt;The stated goal is to close the gap between what a site declares and what it enforces. As the announcement puts it: "When your stated preferences and your enforced rules disagree, some crawlers treat it as a basis to disregard your preferences or try to bypass your enforced rules." That is a fair description of the problem. It is also the reason the custom-rules carve-out below matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part the announcement does not lead with
&lt;/h2&gt;

&lt;p&gt;Three limits sit further down the post, and each one changes an operational decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Category-wide only.&lt;/strong&gt; Cloudflare states plainly that Bot Preference Sync "is designed to tackle policy decisions made category-wide rather than case-by-case, it will not directly read from individual custom rules with more complex logic." If you have negotiated an exception for one named crawler - a licensing deal, a partner index, an internal scraper - that exception lives in a custom rule and will not appear in the generated file. Your &lt;code&gt;robots.txt&lt;/code&gt; will then advertise a stricter policy than your edge actually enforces, which is the exact mismatch the feature was built to remove. Cloudflare's answer is to turn the sync off and hand-maintain the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On by default for new customers.&lt;/strong&gt; The post says Bot Preference Sync "will be on by default" for all new customers. Read alongside the next paragraph, that means the sync is on, not that blocks are on: for non-publisher new customers Cloudflare "will not have any blocks or disallows added by default". The two statements are consistent, but they read as contradictory on a first pass, and the practical effect is that a newly onboarded zone gets a Cloudflare-managed &lt;code&gt;robots.txt&lt;/code&gt; file whether or not it has any AI policy set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A new publisher default.&lt;/strong&gt; At onboarding, a customer can select "I monetize from pages with ads on this domain", which sets Training to Disallow as the default. That is an opt-in checkbox with a policy consequence, changeable at any time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The docs have not caught up
&lt;/h2&gt;

&lt;p&gt;The AI Crawl Control documentation set does not contain a Bot Preference Sync page. Requesting &lt;code&gt;/ai-crawl-control/features/managed-robots-txt/&lt;/code&gt; returns Cloudflare's 404 template. The navigation lists Analyze AI traffic, Manage AI crawlers, and Directives - the tab formerly called Robots.txt - and nothing else in that area. The Directives page itself carries a "Last updated Apr 23, 2026" stamp and still points its managed-status card at the older &lt;code&gt;bots/additional-configurations/managed-robots-txt/&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;That older page, stamped 3 August 2026 in its own structured data, documents a materially different file. It writes a fixed content-signal line and a blanket disallow for a named list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent: *
Content-signal: search=yes, ai-train=no, use=reference
Allow: /

User-agent: Amazonbot
Disallow: /

User-agent: Applebot-Extended
Disallow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full list in that document is 8 crawlers: Amazonbot, Applebot-Extended, Bytespider, CCBot, ClaudeBot, Google-Extended, GPTBot, and meta-externalagent. Turn on the legacy toggle today - Security Settings, filter by Bot traffic, "Set your preference to block training in robots.txt" - and that is what lands in your file. It is not the Search/Agent/Training model the 21 August post describes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;Legacy managed robots.txt (doc updated 3 Aug 2026)&lt;/th&gt;
&lt;th&gt;Bot Preference Sync (announced 21 Aug 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of directives&lt;/td&gt;
&lt;td&gt;Fixed list of 8 named crawlers&lt;/td&gt;
&lt;td&gt;BotBase categories, periodically refreshed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy model&lt;/td&gt;
&lt;td&gt;Block training, one setting&lt;/td&gt;
&lt;td&gt;Search, Agent, Training set separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads your custom rules&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No - category-wide only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Placement in your file&lt;/td&gt;
&lt;td&gt;Prepended above existing content&lt;/td&gt;
&lt;td&gt;Prepended above existing content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;Live page, all plans&lt;/td&gt;
&lt;td&gt;No page; 404 at the expected path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why the 15 September date is the real deadline
&lt;/h2&gt;

&lt;p&gt;Bot Preference Sync publishes a preference. It does not decide who gets blocked at the edge, and the edge rules change on a fixed date.&lt;/p&gt;

&lt;p&gt;In "Your site, your rules: new AI traffic options for all customers", published 1 July 2026, Cloudflare wrote: "On September 15, 2026, we'll be setting new defaults for each of these three classifications." For all new domains onboarding to Cloudflare, Training and Agent are blocked by default on pages that display ads, while Search stays allowed. The same post carries the sentence that costs the most money if you miss it: multi-purpose crawlers will be "allowed/blocked according to all of their behaviors", with the most restrictive applicable rule winning, so "multi-purpose crawlers such as Googlebot, Applebot, and BingBot will be blocked by customers who have selected to block Training (either through the new options to manage AI traffic, or through the legacy Block AI bots service)."&lt;/p&gt;

&lt;p&gt;Read that twice if you turned on "Block AI bots" in 2025 and have not looked since. Cloudflare's opt-out is a setting in the zone's Security settings, available "any time leading up to September 15", which confirms you want no change to Training crawlers that also crawl for Search.&lt;/p&gt;

&lt;p&gt;The 21 August post adds a partial escape hatch on the preference side. The Training &lt;strong&gt;Disallow&lt;/strong&gt; option writes a "no training" preference rather than a hard block, and Cloudflare says cooperating mixed-use crawlers that meet four transparency requirements keep search access. Those four requirements, verbatim from the post, are that the bot respects a "no training" preference in &lt;code&gt;robots.txt&lt;/code&gt; by any mechanism; gives site owners a way to opt out of AI summaries; provides URL-level visibility into which pages were made available for training plus metrics on search results; and can show publicly that disallowing training does not hurt traditional search results. Operators meeting them are tracked in the AI bot transparency section on &lt;a href="https://radar.cloudflare.com/ai-insights" rel="noopener noreferrer"&gt;Cloudflare Radar&lt;/a&gt;. Operators that do not "are still blocked when you disallow training".&lt;/p&gt;

&lt;p&gt;So the preference layer and the enforcement layer resolve differently, on different dates, using different lists. That is the whole problem for anyone measuring AI search visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell whether this is you
&lt;/h2&gt;

&lt;p&gt;Four checks, none of which take longer than a few minutes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch your own &lt;code&gt;robots.txt&lt;/code&gt; and look for &lt;code&gt;# BEGIN Cloudflare Managed content&lt;/code&gt; or &lt;code&gt;# BEGIN Cloudflare Bot Preference Sync&lt;/code&gt;. Either marker means Cloudflare is writing part of your file.&lt;/li&gt;
&lt;li&gt;Look for a &lt;code&gt;Content-signal:&lt;/code&gt; line. Cloudflare's managed content sets &lt;code&gt;search=yes, ai-train=no, use=reference&lt;/code&gt;. The &lt;code&gt;use&lt;/code&gt; field is an optional extension Cloudflare describes as under test, with three values: &lt;code&gt;use=immediate&lt;/code&gt; (interact, store and reuse nothing), &lt;code&gt;use=reference&lt;/code&gt; (index, excerpt, and link back), and &lt;code&gt;use=full&lt;/code&gt; (summarize and reproduce).&lt;/li&gt;
&lt;li&gt;Open Security Settings and check whether "Block AI bots" or the legacy training-preference toggle is on. If it is, the 15 September multi-purpose rule applies to you.&lt;/li&gt;
&lt;li&gt;Check whether any per-crawler exception you rely on lives in a custom rule. If it does, Bot Preference Sync will not represent it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One more thing worth knowing before anyone panics about search Console noise: Cloudflare's own documentation states that "Google Search Console may occasionally report &lt;code&gt;Syntax not understood&lt;/code&gt; for Content Signals and newer directives in the robots.txt standard. However, we have observed no impact on crawling rates or SEO as a result of these reports." That is Cloudflare's observation, not Google's statement, and it is worth treating as a vendor claim rather than a settled fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  India-specific considerations
&lt;/h2&gt;

&lt;p&gt;Indian publishers and D2C brands sitting on Cloudflare's Free tier are affected by a detail that is easy to miss. Free-plan domains that have no &lt;code&gt;robots.txt&lt;/code&gt; of their own and do not use the managed feature are served Cloudflare's Content Signals Policy text when a crawler requests the file. That policy defines the &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;ai-input&lt;/code&gt; and &lt;code&gt;ai-train&lt;/code&gt; categories and explicitly reserves rights under Article 4 of EU Directive 2019/790, but it expresses no preference of its own. A site owner in Gurugram or Bengaluru reading their own &lt;code&gt;robots.txt&lt;/code&gt; may reasonably conclude they have opted out of training when they have not.&lt;/p&gt;

&lt;p&gt;For teams whose Indian traffic depends on AI-assisted discovery rather than classical blue links, the practical order is: decide the Training position first, confirm the 15 September opt-out state second, and only then decide whether to let Cloudflare own the file. &lt;code&gt;robots.txt&lt;/code&gt; compliance remains voluntary in every case - Cloudflare's documentation says so directly: the file "expresses your preferences, but it does not prevent crawlers from accessing your content at a technical level."&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still unknown
&lt;/h2&gt;

&lt;p&gt;Cloudflare has not published the generated user-agent list for any category, the refresh cadence for that list beyond "periodically", the API or Terraform surface for Bot Preference Sync, or a dated availability commitment beyond "in the coming week". There is no changelog entry in the AI Crawl Control changelog as of this writing. Until the documentation page exists, the only authoritative description of the feature is a blog post, and blog posts are not a contract.&lt;/p&gt;

&lt;p&gt;The safe operational position for the next fortnight is unchanged: keep ownership of the file you can diff. If you have a hand-maintained &lt;code&gt;robots.txt&lt;/code&gt; with per-crawler exceptions, leave the sync off until the docs land, and re-check your setting on 14 September. The real cost here is usually the audit after the fact, not the toggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is Cloudflare Bot Preference Sync?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bot Preference Sync is a Cloudflare feature announced on 21 August 2026 that generates or updates your robots.txt file to match the Search, Agent and Training policies you set in the zone dashboard. Cloudflare says it will be available on every plan tier, from Free to Enterprise, in the week following the announcement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Bot Preference Sync delete my existing robots.txt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Cloudflare states that its generated content is prepended to your existing material, so any Disallow directives already in your file are maintained. The added block sits between the markers BEGIN Cloudflare Bot Preference Sync and END Cloudflare Bot Preference Sync, above whatever your origin already serves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will it reflect my custom crawler rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Cloudflare says the feature handles policy decisions made category-wide rather than case-by-case, and that it will not directly read from individual custom rules with more complex logic. If you rely on a per-crawler exception, turn the sync off and maintain the file yourself, or your published preference will not match your enforcement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes on 15 September 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare's 1 July 2026 post set new defaults from that date. For all new domains, Training and Agent are blocked by default on pages that display ads, while Search stays allowed. Multi-purpose crawlers are then judged on all their behaviours, with the most restrictive applicable rule winning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will Googlebot be blocked if I block training?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare's 1 July 2026 post says multi-purpose crawlers such as Googlebot, Applebot and BingBot will be blocked by customers who have selected to block Training, including through the legacy Block AI bots service. Cloudflare offers an opt-out in zone Security settings, available at any time before 15 September 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are the four transparency requirements for mixed-use bots?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Per the 21 August post, a Search-and-Training bot must respect a no-training preference in robots.txt by any mechanism, give site owners a way to opt out of AI summaries, provide URL-level visibility into pages made available for training plus search metrics, and show publicly that disallowing training does not harm traditional search results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there documentation for Bot Preference Sync?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not yet. The expected path under AI Crawl Control returns a 404, and the AI Crawl Control changelog carries no entry. The page that does exist documents the older managed robots.txt setting, stamped 3 August 2026, which writes a blanket Disallow for eight named crawlers instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does robots.txt actually stop AI crawlers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Cloudflare's own documentation says compliance is voluntary: the file expresses preferences but does not prevent access at a technical level, and some operators disregard Disallow directives. Enforcement requires edge blocking through AI Crawl Control or WAF rules, which is a separate configuration from the preference file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How eCorpIT can help
&lt;/h2&gt;

&lt;p&gt;eCorpIT runs technical AI-search audits for teams whose discovery now depends on answer engines as much as on classical search results, and this is exactly the class of change those audits catch: a vendor default that rewrites a file nobody on the team owns. Our &lt;a href="https://ecorpit.com/seo-aeo-geo-audit-service/" rel="noopener noreferrer"&gt;SEO, AEO and GEO audit service&lt;/a&gt; checks the served &lt;code&gt;robots.txt&lt;/code&gt;, the content-signal state, the edge rules behind it, and whether the two agree. eCorpIT is CMMI Level 5, MSME certified and ISO 27001:2022 certified. Tell us your Cloudflare plan tier and current Training setting at &lt;a href="https://ecorpit.com/contact-us/" rel="noopener noreferrer"&gt;/contact-us/&lt;/a&gt; and we will tell you what changes on 15 September.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Cloudflare Blog, &lt;a href="https://blog.cloudflare.com/bot-preference-sync/" rel="noopener noreferrer"&gt;Say it once: introducing Bot Preference SynC&lt;/a&gt;, 21 August 2026.&lt;/li&gt;
&lt;li&gt;Cloudflare Blog, &lt;a href="https://blog.cloudflare.com/content-independence-day-ai-options/" rel="noopener noreferrer"&gt;Your site, your rules: new AI traffic options for all customers&lt;/a&gt;, 1 July 2026.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/bots/additional-configurations/managed-robots-txt/" rel="noopener noreferrer"&gt;robots.txt setting&lt;/a&gt;, last updated 3 August 2026.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/ai-crawl-control/" rel="noopener noreferrer"&gt;AI Crawl Control overview&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/ai-crawl-control/features/track-robots-txt/" rel="noopener noreferrer"&gt;Directives (formerly Robots.txt)&lt;/a&gt;, last updated 23 April 2026.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/ai-crawl-control/features/manage-ai-crawlers/" rel="noopener noreferrer"&gt;Manage AI crawlers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/bots/botbase/" rel="noopener noreferrer"&gt;BotBase&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/bots/concepts/bot/verified-bots/" rel="noopener noreferrer"&gt;Verified bots&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/ai-crawl-control/changelog/" rel="noopener noreferrer"&gt;AI Crawl Control changelog&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Radar, &lt;a href="https://radar.cloudflare.com/ai-insights" rel="noopener noreferrer"&gt;AI Insights and AI bot transparency&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Radar, &lt;a href="https://radar.cloudflare.com/bots/directory" rel="noopener noreferrer"&gt;public bots directory&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cloudflare Docs, &lt;a href="https://developers.cloudflare.com/ai-crawl-control/configuration/ai-crawl-control-with-waf/" rel="noopener noreferrer"&gt;AI Crawl Control with Cloudflare WAF&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Last updated: 22 August 2026.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>robotstxt</category>
      <category>aicrawlers</category>
      <category>geo</category>
    </item>
    <item>
      <title>Alibaba Tumbles on Earnings Miss; Tesla Robotaxi Approval and Cloudflare Agent Surge Boost AI Stocks</title>
      <dc:creator>StartupHub.ai</dc:creator>
      <pubDate>Fri, 21 Aug 2026 23:26:03 +0000</pubDate>
      <link>https://dev.to/startuphubai__c637ac1b0/alibaba-tumbles-on-earnings-miss-tesla-robotaxi-approval-and-cloudflare-agent-surge-boost-ai-stocks-j07</link>
      <guid>https://dev.to/startuphubai__c637ac1b0/alibaba-tumbles-on-earnings-miss-tesla-robotaxi-approval-and-cloudflare-agent-surge-boost-ai-stocks-j07</guid>
      <description>&lt;h1&gt;
  
  
  Alibaba Tumbles on Earnings Miss; Tesla Robotaxi Approval and Cloudflare Agent Surge Boost AI Stocks — alibaba tumbles earnings miss tesla robotaxi
&lt;/h1&gt;

&lt;p&gt;In a day marked by significant sector-specific movements, Alibaba Group Holding Ltd. experienced a sharp decline, while Tesla Inc. and Cloudflare Inc. saw substantial gains, highlighting the dynamic nature of the AI stock landscape. This analysis delves into the key factors driving these shifts, including Alibaba's earnings miss, Tesla's robotaxi milestone, and Cloudflare's growing role in AI-agent traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alibaba's Earnings Setback and AI Spending Concerns
&lt;/h2&gt;

&lt;p&gt;Alibaba (NYSE: BABA) slid 8.6% to $119.34, its most significant single-day drop in months. This downturn followed the release of its fiscal Q1 2027 results, which revealed adjusted earnings per ADS of $1.26, a 42% year-over-year decrease and significantly below the $1.85 consensus estimate. While cloud revenue growth reached its fastest pace in 22 quarters at 45%, investors were concerned by the revelation that AI capital expenditure is currently running at 4.5 times the rate of new cloud revenue growth. This imbalance between aggressive infrastructure spending and near-term returns is a pattern observed across the AI sector, as tracked by StartupHub.ai's global database of over 91,000 companies. For Alibaba to regain investor confidence, management must demonstrate a clearer path to narrowing this spending-revenue gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tesla Secures Key Robotaxi Approval
&lt;/h2&gt;

&lt;p&gt;Tesla Inc. (NASDAQ: TSLA) surged 5.1% to $362.86, extending its recent positive momentum. The catalyst for this rise was the approval granted by Nevada regulators for up to 5,000 Tesla robotaxis to operate in Las Vegas. This permit signifies a significant step towards a commercial rollout of Tesla's autonomous fleet, placing it ahead of many competitors. Concurrently, Tesla announced plans to showcase its electric Semi truck in Germany, underscoring its expanding presence in the commercial vehicle market. CEO Elon Musk remains optimistic about Tesla's ability to surpass Wall Street's revenue forecasts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Dominates AI-Agent Network Traffic
&lt;/h2&gt;

&lt;p&gt;Cloudflare Inc. (NYSE: NET) climbed 5.1% to $293.14 after exceeding second-quarter expectations and raising its full-year guidance. A pivotal announcement was that non-human and AI-agent traffic now constitutes over 50% of its total network volume. This milestone positions Cloudflare as a critical infrastructure provider for the burgeoning wave of agentic AI. Operating at the nexus of AI inference delivery and zero-trust security, Cloudflare is well-positioned to capitalize on key enterprise IT spending trends in 2026. The stock has seen a year-to-date increase of 49.6%, making it a notable player for investors seeking exposure to AI-agent infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broader Market Movements and Key AI Stocks
&lt;/h2&gt;

&lt;p&gt;Other notable movers included SoundHound AI Inc. (NASDAQ: SOUN), which added 5.0%; Snowflake Inc. (NYSE: SNOW), up 3.6% driven by AI data platform spending; and Palantir Technologies Inc. (NYSE: PLTR), which rose 3.4% to continue a strong monthly performance. On the downside, Arm Holdings plc (NASDAQ: ARM) dropped 3.0%, and Intel Corp. (NASDAQ: INTC) slid 2.2%. &lt;/p&gt;

&lt;p&gt;The broader AI stock universe, encompassing hardware, hyperscale cloud, and enterprise SaaS, continues to focus on NVIDIA Corp. (NVDA), Broadcom Inc. (AVGO), and Taiwan Semiconductor Manufacturing Co. (TSM) for hardware; Microsoft Corp. (MSFT) and Alphabet Inc. (GOOGL) for cloud infrastructure; and Palantir (PLTR) and Oracle Corp. (ORCL) for enterprise AI software. The market's attention has shifted from AI training clusters to inference delivery, AI-agent monetization, and autonomous deployment. The upcoming NVIDIA Q2 FY27 earnings report on August 26 is anticipated as a major catalyst for the sector. Investors are also closely watching the Jackson Hole Economic Symposium for signals on future interest rate policy, which could impact rate-sensitive tech stocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Navigating the AI Investment Landscape
&lt;/h2&gt;

&lt;p&gt;The market's reaction to Alibaba's earnings miss, coupled with positive developments for Tesla and Cloudflare, underscores the evolving dynamics within the AI sector. While concerns about capital expenditure pacing persist, significant advancements in autonomous technology and AI infrastructure continue to drive innovation and investment. As the sector matures, the focus remains on companies demonstrating clear monetization strategies and sustainable growth in the rapidly expanding AI ecosystem. The upcoming earnings reports and economic symposiums will be crucial in shaping market sentiment for the remainder of 2026. Investors looking for insights into the broader market trends, including the &lt;a href="https://www.startuphub.ai/ai-news/ai-stocks-daily/2026/ai-stocks-2026-07-23" rel="noopener noreferrer"&gt;tesla alphabet drag mega-cap lower earnings&lt;/a&gt;, will find valuable context in ongoing analyses.&lt;/p&gt;

&lt;p&gt;This article was originally published on StartupHub.ai.&lt;/p&gt;

</description>
      <category>alibaba</category>
      <category>tesla</category>
      <category>cloudflare</category>
      <category>aistocks</category>
    </item>
    <item>
      <title>Your Worker Returned 500 and the Log Says `outcome: "ok"`</title>
      <dc:creator>ushiro</dc:creator>
      <pubDate>Fri, 21 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/ai_changewatch/your-worker-returned-500-and-the-log-says-outcome-ok-2dla</link>
      <guid>https://dev.to/ai_changewatch/your-worker-returned-500-and-the-log-says-outcome-ok-2dla</guid>
      <description>&lt;p&gt;I run &lt;a href="https://aichangewatch.com/?src=devto" rel="noopener noreferrer"&gt;&lt;strong&gt;AI Change Watch&lt;/strong&gt;&lt;/a&gt;, a small independent project that&lt;br&gt;
crawls what 15 AI vendors publish about their own models — deprecation tables, lifecycle pages, pricing&lt;br&gt;
and SDK releases — and records every time one of them changes.&lt;/p&gt;

&lt;p&gt;It runs on Cloudflare Workers, which means that when someone tells me "your site 500'd an hour ago",&lt;br&gt;
the obvious tool is useless. &lt;code&gt;wrangler tail&lt;/code&gt; is a &lt;strong&gt;live stream&lt;/strong&gt;. It shows you what is happening now.&lt;br&gt;
It cannot show you an hour ago.&lt;/p&gt;

&lt;p&gt;There is a way to read the past, and there are four traps in it that cost me most of a day.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that works
&lt;/h2&gt;

&lt;p&gt;Workers can write their invocation logs to a queryable store, and a REST endpoint reads it back. First&lt;br&gt;
the worker has to be opted in — this is the whole config:&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="c1"&gt;// wrangler.jsonc&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;"observability"&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;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Then you can ask for events in a time range:&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;-sX&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/accounts/&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT&lt;/span&gt;&lt;span class="s2"&gt;/workers/observability/telemetry/query"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CF_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "queryId": "anything",
    "timeframe": { "from": 1786000000000, "to": 1786003600000 },
    "limit": 500,
    "view": "events",
    "parameters": {
      "datasets": ["cloudflare-workers"],
      "filters": [
        { "id": "f1", "key": "$workers.event.response.status",
          "type": "number", "operation": "eq", "value": 500 }
      ]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; are &lt;strong&gt;epoch milliseconds&lt;/strong&gt;, not ISO strings. A read-scoped API token is enough — the&lt;br&gt;
one I already had for deploys worked unchanged.&lt;/p&gt;

&lt;p&gt;Each event carries more than you would guess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$workers.outcome                              ok | exceededCpu | canceled
$workers.cpuTimeMs  /  $workers.wallTimeMs
$workers.event.request.path  /  .search
$workers.event.request.headers['user-agent']
$workers.event.request.cf.asOrganization      the ASN owner
$workers.event.response.status
$metadata.error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the tool. Now the traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: a rendered 500 is a successful invocation
&lt;/h2&gt;

&lt;p&gt;I started by filtering on the field that sounds right:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$workers.outcome"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"exceededCpu"&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;and found nothing, repeatedly, while the site was demonstrably returning 500s.&lt;/p&gt;

&lt;p&gt;Because for every one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"ok"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker ran. It produced a response. It returned it. That the response was an error page is not the&lt;br&gt;
runtime's problem — the invocation succeeded. &lt;strong&gt;&lt;code&gt;outcome&lt;/code&gt; describes the worker, not the HTTP result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;outcome&lt;/code&gt; is the wrong axis for application errors. Filter on &lt;code&gt;$workers.event.response.status&lt;/code&gt; for&lt;br&gt;
what the user saw, and read &lt;code&gt;$metadata.error&lt;/code&gt; for the throw. &lt;code&gt;outcome&lt;/code&gt; is for failures the runtime&lt;br&gt;
itself noticed: CPU limit, cancellation.&lt;/p&gt;

&lt;p&gt;This is worth internalising because it inverts the usual relationship. In most stacks "the request&lt;br&gt;
failed" and "the handler failed" are the same event. At the edge they are two different fields — and&lt;br&gt;
the one with the friendlier name is the one that will not tell you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 2: a wide time window silently undercounts
&lt;/h2&gt;

&lt;p&gt;This is the one that actually cost me the day.&lt;/p&gt;

&lt;p&gt;I asked for 5xx across a 24-hour window, got nine events, and concluded I was chasing a single bug. The&lt;br&gt;
same 24 hours, walked in 4-hour slices and concatenated, returned &lt;strong&gt;956&lt;/strong&gt; — across 92 URLs and three&lt;br&gt;
unrelated causes.&lt;/p&gt;

&lt;p&gt;I re-ran the comparison today, on 404s, to check it was not a one-off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24h asked as one query      →  26 events
same 24h in 4h slices       → 266 events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The single query returned 10% of what was there.&lt;/strong&gt; Not a rounding difference — a different&lt;br&gt;
conclusion. And nothing in the response says so: no truncation flag, no "results were sampled" field.&lt;br&gt;
You get a well-formed answer that happens to be mostly missing.&lt;/p&gt;

&lt;p&gt;So the loop, not the query:&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;out&lt;/span&gt; &lt;span class="o"&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;let&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;4&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="k"&gt;from&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="nx"&gt;_000&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;to&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="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="nx"&gt;_000&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;ev&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;queryEvents&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&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="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`slice &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;h hit the limit — narrow it`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;ev&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 &lt;code&gt;ev.length &amp;gt;= 500&lt;/code&gt; check matters as much as the slicing. A slice that returns exactly your limit is&lt;br&gt;
truncated, and you have to narrow &lt;em&gt;that&lt;/em&gt; slice further. Without the warning you cannot tell "500 events&lt;br&gt;
happened" from "500 events fit".&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 3: &lt;code&gt;exists&lt;/code&gt; matches empty strings, and &lt;code&gt;includes&lt;/code&gt; ignores case
&lt;/h2&gt;

&lt;p&gt;Two smaller ones, both of which produced confidently wrong numbers before I noticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;operation: "exists"&lt;/code&gt; matches a key that is present but empty.&lt;/strong&gt; I wanted requests Cloudflare had&lt;br&gt;
identified as verified bots:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$workers.event.request.cf.verifiedBotCategory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"exists"&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;That field is present on every request and is &lt;code&gt;""&lt;/code&gt; on almost all of them, so the filter matched the&lt;br&gt;
entire dataset and I briefly believed the whole site was bot traffic. Use &lt;code&gt;exists&lt;/code&gt; only for keys&lt;br&gt;
genuinely absent on what you are excluding — &lt;code&gt;sec-fetch-mode&lt;/code&gt; is a real example, since non-browsers do&lt;br&gt;
not send it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;operation: "includes"&lt;/code&gt; is case-insensitive.&lt;/strong&gt; Filtering user agents for &lt;code&gt;bot&lt;/code&gt; and for &lt;code&gt;Bot&lt;/code&gt; returned&lt;br&gt;
the identical 3,181 events. Convenient once you know; misleading if you were using case to separate two&lt;br&gt;
populations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: event counts are not invocation counts
&lt;/h2&gt;

&lt;p&gt;The events view and the dashboard's invocation count disagree, and both are right. On one day my web&lt;br&gt;
worker showed &lt;strong&gt;24,085 telemetry events against 8,772 invocations&lt;/strong&gt; — roughly 2.7 events per&lt;br&gt;
invocation.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;proportions&lt;/strong&gt; — "what share of requests were 404s", "which UA dominates" — take from the events view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;absolute totals&lt;/strong&gt; — "how many requests did this worker serve" — take from
&lt;code&gt;workersInvocationsAdaptive&lt;/code&gt; in the GraphQL analytics API, not from counting events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixing them gives a number that is wrong by a factor you cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually run now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Past-tense debugging: "what 500'd between 3am and 4am".&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;queryEvents&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&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;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&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;r&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;`https://api.cloudflare.com/client/v4/accounts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workers/observability/telemetry/query`&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="na"&gt;Authorization&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;TOKEN&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="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="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="na"&gt;queryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeframe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;from&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;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;datasets&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;cloudflare-workers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;filters&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&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;r&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;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;events&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…called from the slicing loop above, with the results grouped in plain JavaScript rather than by asking&lt;br&gt;
the API to group them. (&lt;code&gt;view: "calculations"&lt;/code&gt; with a &lt;code&gt;groupBy&lt;/code&gt; on a high-cardinality key returns only a&lt;br&gt;
few groups, quietly — the same failure mode as trap 2: a well-formed answer that is mostly missing.)&lt;/p&gt;

&lt;p&gt;The retention window is limited. I have reliably queried three days back and would not build a workflow&lt;br&gt;
that assumes more; for anything you need to keep, pull it out and store it yourself.&lt;/p&gt;

&lt;p&gt;One more, learned the embarrassing way: &lt;strong&gt;&lt;code&gt;cf.asOrganization&lt;/code&gt; is the ASN owner, not the bot.&lt;/strong&gt; Requests&lt;br&gt;
from "Anthropic, PBC" turned out to be a crawler that robots.txt already allowed, and "Amazon&lt;br&gt;
Technologies" was PerplexityBot. Identify declared crawlers by user agent; use the ASN only to catch&lt;br&gt;
traffic whose user agent is lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wrangler tail&lt;/code&gt; is for watching. For asking, use the telemetry API — and remember that &lt;strong&gt;&lt;code&gt;outcome: "ok"&lt;/code&gt;&lt;br&gt;
means the worker succeeded, not that your user did&lt;/strong&gt;, and that a query covering a wide window will hand&lt;br&gt;
you a confident answer built from a tenth of the data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tracker this came out of is at &lt;a href="https://aichangewatch.com/?src=devto" rel="noopener noreferrer"&gt;aichangewatch.com&lt;/a&gt; — it watches AI&lt;br&gt;
vendor docs for changes, and the 500s that started all this were a REST detail endpoint quietly falling&lt;br&gt;
through to its collection endpoint.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Running a Monitoring SaaS on Cloudflare Workers + Supabase for Almost Nothing</title>
      <dc:creator>Merlonix</dc:creator>
      <pubDate>Fri, 21 Aug 2026 10:41:00 +0000</pubDate>
      <link>https://dev.to/merlonix/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing-1hkm</link>
      <guid>https://dev.to/merlonix/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing-1hkm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://merlonix.com/blog/monitoring-saas-on-cloudflare-workers-supabase/" rel="noopener noreferrer"&gt;Merlonix blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Merlonix monitors uptime, SSL/TLS, DNS, email authentication, blacklists, Certificate Transparency, Core Web Vitals, and MCP servers for agencies. A monitoring product has an unforgiving shape: it must run &lt;em&gt;continuously&lt;/em&gt;, hit &lt;em&gt;arbitrary customer-supplied hostnames&lt;/em&gt;, and stay up &lt;em&gt;more reliably than the things it watches&lt;/em&gt; — while, in our case, keeping the infrastructure bill within a rounding error of zero until revenue exists to justify more.&lt;/p&gt;

&lt;p&gt;This post is the real architecture, including the parts that bit us. Nothing here is a reference design we aspire to; every component named below is deployed and verifiable from the outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the system
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compute: eight Cloudflare Workers.&lt;/strong&gt; One HTTP API worker (Hono) serves everything under &lt;code&gt;api.merlonix.com&lt;/code&gt;. Seven background workers do the actual monitoring: a &lt;strong&gt;scheduler&lt;/strong&gt; (cron), a &lt;strong&gt;check-runner&lt;/strong&gt; and &lt;strong&gt;vendor-runner&lt;/strong&gt; (queue consumers that execute checks), a &lt;strong&gt;vendor-fetcher&lt;/strong&gt;, a &lt;strong&gt;dlq-consumer&lt;/strong&gt; (dead-letter forensics), a &lt;strong&gt;browser-runner&lt;/strong&gt;, and a &lt;strong&gt;db-backup&lt;/strong&gt; worker. Each has its own wrangler config and its own deploy verifier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend: two static Cloudflare Pages projects.&lt;/strong&gt; The marketing site and the app are both Next.js &lt;em&gt;static exports&lt;/em&gt; — no SSR servers, no origin to fall over. Anything dynamic goes through the API worker. A Pages Function provides the thin middleware layer (redirects, custom-domain status-page routing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Glue: Cloudflare Queues.&lt;/strong&gt; The scheduler enqueues due work onto &lt;code&gt;checks-q&lt;/code&gt; and &lt;code&gt;vendor-q&lt;/code&gt;; the runners consume in batches of 10. Failures retry, and exhausted retries land in a dead-letter queue with a consumer that records the forensic payload instead of dropping it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State: one Supabase Postgres.&lt;/strong&gt; Every table that holds tenant data runs with &lt;strong&gt;forced row-level security&lt;/strong&gt; — the API worker uses the service role deliberately and narrowly, and RLS is audited by a script that enumerates deny-all tables and cross-tenant probes. The migration ledger is past 270 forward-only migrations, applied to production by an idempotent runner. There is no second database; the queue messages carry IDs, and Postgres is the single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The revenue-gated cron throttle
&lt;/h2&gt;

&lt;p&gt;The scheduler's production cron fires &lt;strong&gt;every minute&lt;/strong&gt;. But firing and working are different things:&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="cm"&gt;/** Revenue-gated SLA throttle. */&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;isSweepDue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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="nx"&gt;hasLiveSubscription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;hasLiveSubscription&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;return&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCMinutes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no live customer, a sweep only does real work on every 5th UTC minute — byte-for-byte the enqueue volume of a 5-minute cron, which costs effectively nothing while there's nobody to monitor but seeded assets. The moment any revenue-bearing subscription exists, the &lt;em&gt;very next tick&lt;/em&gt; restores full per-minute cadence. No redeploy, no flag flip, no human. The SLA follows the money automatically.&lt;/p&gt;

&lt;p&gt;The interesting part is the failure mode we shipped and later fixed. The &lt;code&gt;hasLiveSubscription&lt;/code&gt; check queries the subscriptions table, and the original catch block was bare:&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="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;hasLiveSubscription&lt;/span&gt; &lt;span class="o"&gt;=&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;Read that as an SRE: a transient database blip on this one query would &lt;em&gt;silently throttle paying customers' monitoring cadence from 1 minute back to 5&lt;/em&gt; — no error, no alert, checks just quietly late. It's the worst kind of degradation: invisible, revenue-adjacent, and plausible-deniable. The fix keeps the safe degradation (bootstrap mode beats crashing the sweep) but fans the failure out to Sentry and structured logs under its own error code, so a sustained degradation pages before a customer notices. If you take one pattern from this post: &lt;strong&gt;when you degrade gracefully, make the degradation loud.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SSRF-guarding the public probes
&lt;/h2&gt;

&lt;p&gt;Our free tools and monitoring checks fetch URLs and hostnames that &lt;em&gt;strangers type into a form&lt;/em&gt;. That is the textbook server-side request forgery setup: the classic target is &lt;code&gt;169.254.169.254&lt;/code&gt;, the cloud metadata endpoint, where a successful internal request leaks the execution environment's credentials.&lt;/p&gt;

&lt;p&gt;Workers add a twist: &lt;code&gt;fetch()&lt;/code&gt; doesn't expose the resolved IP and gives you no way to pin one. So the guard works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-resolve via DNS-over-HTTPS&lt;/strong&gt; (1.1.1.1) and check &lt;em&gt;every&lt;/em&gt; A/AAAA record against a blocklist of loopback, RFC1918, link-local (metadata!), CGNAT, unspecified, and IPv6 unique-local/link-local ranges. Any private answer → reject before fetching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects are followed manually, once.&lt;/strong&gt; A malicious server can 302 to &lt;code&gt;http://127.0.0.1&lt;/code&gt;; we re-run the full SSRF check on the &lt;code&gt;Location&lt;/code&gt; header before following, and a second hop is always rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responses are size-capped and time-capped&lt;/strong&gt; so a hostile endpoint can't stall a worker or balloon memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the honest residual, documented in the code rather than papered over: the runtime's &lt;code&gt;fetch()&lt;/code&gt; performs its &lt;em&gt;own&lt;/em&gt; DNS resolution, so a sub-TTL attacker could answer our DoH probe with a public IP and the runtime with a private one. IP-pinning isn't viable on Workers (TLS validates against SNI). The authoritative backstop is Cloudflare's platform egress policy — Workers cannot open connections into loopback/RFC1918/link-local regardless of DNS. Our DoH layer is defense-in-depth on top of that, and we say so, because an SSRF guard you overstate is worse than one you understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching the watcher, from a different cloud
&lt;/h2&gt;

&lt;p&gt;A monitoring company that monitors itself with itself has a bootstrapping problem: if Cloudflare has an account-level bad day, the thing that would tell us is also having a bad day.&lt;/p&gt;

&lt;p&gt;So the external watchdog doesn't run on Cloudflare at all. It runs &lt;em&gt;inside Supabase&lt;/em&gt; — which sits on AWS — using &lt;code&gt;pg_cron&lt;/code&gt; plus the &lt;code&gt;http&lt;/code&gt; extension: every 5 minutes, a &lt;code&gt;SECURITY DEFINER&lt;/code&gt; function curls &lt;code&gt;merlonix.com&lt;/code&gt; and the API's &lt;code&gt;/healthz&lt;/code&gt; and &lt;code&gt;/readyz&lt;/code&gt; endpoints, tracks consecutive failures in a table, and posts to an operator Discord webhook after a sustained-failure threshold, with a single recovery notice when things come back. No third-party account, no additional bill, and — the actual point — &lt;strong&gt;no shared fate with the platform it watches&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring reliability plumbing that earns its keep
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every worker invocation writes a heartbeat row&lt;/strong&gt; to a &lt;code&gt;worker_runs&lt;/code&gt; ledger (worker name, outcome, duration). "Is the scheduler actually running?" is a SQL query, not a guess — and a failed heartbeat &lt;em&gt;insert&lt;/em&gt; has its own alarm code, because observability that fails silently isn't observability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every deploy has a verifier script&lt;/strong&gt;, and deploys aren't done until it passes: the API verifier checks live routes and CORS, the workers verifier catches a stale or forgotten worker plus queue/cron/binding drift via the Cloudflare REST API, and a headless-browser smoke pass loads the public pages and fails on console errors, first-party network failures, and layout regressions that string-matching verifiers are blind to. Rollback scripts are pre-written; a failed verifier means roll back, not debug-in-prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dead-letter queue has a consumer.&lt;/strong&gt; DLQs with no consumer are where failures go to be forgotten; ours records each exhausted payload so a storm becomes a diagnosable dataset. That mattered the day a decommissioned LLM model name plus a router that hard-threw instead of falling back flooded it — root-caused from the forensics, fixed, regression-guarded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Nearly nothing, and that's a design constraint, not an accident. Static Pages sites are free. Workers requests at our current scale sit comfortably inside the Workers plan floor. Supabase is on the free tier — the watchdog cron and RLS-forced Postgres both fit inside it. The paid-API checks that could cost real money (PageSpeed Insights, LLM-backed features) sit behind explicit flags, daily caps, and per-tenant meters, so the worst case of a bug is a rate-limit, not a bill. The whole stack is engineered so the monthly infrastructure bill stays within a rounding error of zero until customers exist — at which point the same cron throttle that saves money today upgrades their SLA on the next tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building on this stack
&lt;/h2&gt;

&lt;p&gt;The pattern that generalizes: &lt;strong&gt;cron fires cheap and constant; a pure function decides whether the tick does work.&lt;/strong&gt; It gives you a testable throttle (&lt;code&gt;isSweepDue&lt;/code&gt; is three lines and unit-tested), a zero-redeploy upgrade path, and one place where cadence policy lives. Pair it with loud degradation, verify every deploy from the outside, and put your last-resort watchdog on somebody else's cloud.&lt;/p&gt;

&lt;p&gt;The product this architecture serves is &lt;a href="https://merlonix.com/pricing/" rel="noopener noreferrer"&gt;Merlonix&lt;/a&gt; — monitoring for agencies, from uptime and SSL through MCP server health. The &lt;a href="https://merlonix.com/tools/" rel="noopener noreferrer"&gt;free tools&lt;/a&gt; run the same SSRF-guarded probe path described above; you can watch it work without signing up.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a streaming investigation assistant that tool-calls an MCP server and cites case IDs</title>
      <dc:creator>Royal Simpson Pinto</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:30:28 +0000</pubDate>
      <link>https://dev.to/royalpinto007/building-a-streaming-investigation-assistant-that-tool-calls-an-mcp-server-and-cites-case-ids-455n</link>
      <guid>https://dev.to/royalpinto007/building-a-streaming-investigation-assistant-that-tool-calls-an-mcp-server-and-cites-case-ids-455n</guid>
      <description>&lt;p&gt;I keep a public registry of documented AI-agent failures called AgentPostmortem: real incidents where an agent got prompt-injected, deleted a database, or spun into a runaway loop, each written up as a case file with an ID like APM-0048. The registry is useful, but browsing it is a chore. If I want to know why refund agents get prompt-injected, I have to guess search terms, open cases, and read them one by one.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;casebook-chat&lt;/strong&gt;: a chat interface that does the searching for me. You describe an incident or ask about a failure mode, and the assistant searches the live registry, pulls the relevant case files, and answers in plain language while citing the real case IDs it used. The whole thing runs as a single Cloudflare Worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;The registry already speaks MCP (Model Context Protocol). It exposes three tools over JSON-RPC at &lt;code&gt;mcp.agentpostmortem.com/mcp&lt;/code&gt;. Rather than reimplement search or copy the data into a new database, I wanted the chat model to call those tools directly and ground its answers in whatever they returned.&lt;/p&gt;

&lt;p&gt;The three tools map cleanly onto how you actually investigate an incident:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search_cases(query)&lt;/code&gt; runs a full-text search over the failure case files and returns ranked summaries with case IDs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_case(id)&lt;/code&gt; fetches one case in full: outcome, verified facts, unknowns, and lessons.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;similar_failures(description)&lt;/code&gt; takes a plain-language description of an incident and matches it against known failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model decides which to call and when. Ask "why do refund agents get prompt-injected?" and it searches. Say "my agent deleted a database" and it reaches for &lt;code&gt;similar_failures&lt;/code&gt;. Once it has a case ID, it can pull the full detail with &lt;code&gt;get_case&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The stack is deliberately small. The browser runs a Vite and React app using &lt;code&gt;@ai-sdk/react&lt;/code&gt;'s &lt;code&gt;useChat&lt;/code&gt;, which POSTs the message history to &lt;code&gt;/api/chat&lt;/code&gt;. That endpoint is a Hono route inside a Cloudflare Worker, and the same Worker also serves the static UI through Workers assets. One deploy, one origin, no separate backend.&lt;/p&gt;

&lt;p&gt;Inside the route, I call the Vercel AI SDK's &lt;code&gt;streamText&lt;/code&gt; against Groq's &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt;, passing the three tools defined with the AI SDK &lt;code&gt;tool()&lt;/code&gt; helper and zod input schemas. Each tool's &lt;code&gt;execute&lt;/code&gt; is just a &lt;code&gt;fetch&lt;/code&gt; to the MCP server wrapping the arguments in a JSON-RPC &lt;code&gt;tools/call&lt;/code&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="nx"&gt;search_cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Full-text search over documented AI-agent failure cases...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&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="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&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;query&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callMcpTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_cases&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;query&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;Because these are genuine failure investigations, the model often needs more than one hop: search, find a promising case, then fetch its full detail before answering. I allow multi-step tool chains but cap the loop with &lt;code&gt;stopWhen: stepCountIs(5)&lt;/code&gt;, so a model that keeps calling tools cannot run away and burn the whole budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming with tool calls rendered inline
&lt;/h3&gt;

&lt;p&gt;The nice part is the streaming. The result comes back through &lt;code&gt;toUIMessageStreamResponse()&lt;/code&gt;, which turns the run into a UI message stream where tool inputs and outputs arrive as typed message parts, not just text. On the client, &lt;code&gt;useChat&lt;/code&gt; exposes each assistant message as an ordered list of parts, and I render them in order: text parts become markdown, and anything whose type starts with &lt;code&gt;tool-&lt;/code&gt; becomes an inline activity chip.&lt;/p&gt;

&lt;p&gt;Each chip is a collapsible component that reads the part's &lt;code&gt;state&lt;/code&gt; field to show where the call is. While the model is still forming or running the call (&lt;code&gt;input-streaming&lt;/code&gt;, &lt;code&gt;input-available&lt;/code&gt;), it shows a spinner and the word "running". When the output lands (&lt;code&gt;output-available&lt;/code&gt;) it flips to a checkmark and a one-line summary; on failure (&lt;code&gt;output-error&lt;/code&gt;) it shows a cross and the error text. Expand a chip and you see the exact arguments the model sent and the raw result it got back, pretty-printed. So the tool calls appear woven into the answer as it streams, in the order they actually happened, and you can audit every step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Citations that mean something
&lt;/h3&gt;

&lt;p&gt;Grounding is enforced through the system prompt, not wishful thinking. The investigator is told to answer only from what the tools returned, to cite the real case IDs it used, and never to invent case IDs or facts. If the registry returns nothing relevant, it is instructed to say so plainly and label any general-knowledge answer as such. It also calls tools silently instead of narrating "let me search the registry", so the visible answer stays clean while the chips carry the process.&lt;/p&gt;

&lt;p&gt;The MCP client itself is built to never throw. It retries once on a network error or a 5xx, times out each attempt at around eight seconds, and on persistent failure returns a structured "the registry is temporarily unavailable" string that the model is told to relay to the user rather than crash on. It also handles the fact that the MCP server can answer either as JSON or as a &lt;code&gt;text/event-stream&lt;/code&gt;, parsing the last JSON-RPC frame out of the SSE body when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest limitation
&lt;/h2&gt;

&lt;p&gt;The grounding is only as strong as the prompt. There is no post-hoc verification step that checks the case IDs in the final answer against the IDs the tools actually returned. The system prompt tells the model not to invent IDs, and in practice llama-3.3-70b on Groq follows that well, but "the model was instructed not to hallucinate" is a softer guarantee than "the app refuses to emit an ID the tools did not surface". If I wanted a hard guarantee, I would parse the assistant's cited IDs and cross-check them against the tool outputs before rendering. That is the honest gap between "cites case IDs" and "provably cannot fabricate a case ID".&lt;/p&gt;

&lt;p&gt;Two smaller notes: inference runs on the Groq free tier, so rate limits and the occasional slowdown apply, and 429s are caught and surfaced as a friendly "the model is busy" message rather than an error. And the registry data is community-documented, so the answers are only as complete as the casebook behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;What I like about this build is how little glue it needed. An MCP server that already exposes the right tools, an AI SDK that streams typed tool parts, and a single Worker to host both halves. The result is a chat that does not just talk about agent failures but shows its work: every claim traceable to a case file you can open yourself.&lt;/p&gt;

&lt;p&gt;Code is here: &lt;a href="https://github.com/AgentPostmortem/casebook-chat" rel="noopener noreferrer"&gt;https://github.com/AgentPostmortem/casebook-chat&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cloudflare Unveils Kitesurf: AI‑Focused Browser Beats Chromium on Power Use</title>
      <dc:creator>10x Magazine</dc:creator>
      <pubDate>Fri, 21 Aug 2026 06:24:11 +0000</pubDate>
      <link>https://dev.to/10x/cloudflare-unveils-kitesurf-ai-focused-browser-beats-chromium-on-power-use-3ck</link>
      <guid>https://dev.to/10x/cloudflare-unveils-kitesurf-ai-focused-browser-beats-chromium-on-power-use-3ck</guid>
      <description>&lt;p&gt;TL;DR: Cloudflare has unveiled Kitesurf, a cloud‑hosted browser optimized for AI agents, delivering up to 30% lower compute usage than Chromium and streamlining the creation of autonomous web bots.&lt;/p&gt;

&lt;p&gt;Imagine a web browser that never needs a human hand to click, scroll, or type—because it was engineered for machines instead of people. Cloudflare announced exactly that today with Kitesurf, a lightweight, cloud‑native browser designed to run AI‑driven agents at scale. By moving the rendering engine to the edge and stripping out UI layers meant for human interaction, Kitesurf promises faster, cheaper automation for developers building everything from price‑tracking bots to intelligent search assistants.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Kitesurf and Why It Matters?
&lt;/h2&gt;

&lt;p&gt;Kitesurf is not a traditional desktop or mobile browser; it lives entirely in Cloudflare’s global network and exposes a programmable API that AI models can call directly. Instead of loading a full Chromium stack with a graphical interface, Kitesurf runs a headless environment that still supports modern web standards, JavaScript execution, and DOM manipulation. The key difference is its focus on &lt;em&gt;agent‑centric&lt;/em&gt; workloads: tasks like form‑filling, data extraction, and multi‑step navigation that AI systems perform repeatedly.&lt;/p&gt;

&lt;p&gt;Developers can now spin up isolated browser instances with a single API request, attach their preferred language model, and let the agent interact with live sites without worrying about resource contention. Cloudflare markets Kitesurf as a plug‑and‑play solution that removes the need for custom Docker images, Selenium grids, or third‑party headless browsers, reducing operational overhead and security exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Edge Over Traditional Browsers
&lt;/h2&gt;

&lt;p&gt;In internal benchmarks, Cloudflare says Kitesurf consumes roughly 20‑30% less CPU cycles than a comparable Chromium headless instance when executing common automation scripts. The savings stem from two design choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edge‑Hosted Execution&lt;/strong&gt; – By running the browser close to the target website, network latency drops dramatically, and data never traverses the public internet, which cuts both time and bandwidth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trimmed Rendering Pipeline&lt;/strong&gt; – Kitesurf removes UI‑only components such as paint layers, font rasterization for display, and GPU acceleration that are unnecessary for machine‑only interactions. The result is a leaner engine that still respects the same JavaScript event loop and security sandbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For enterprises that run thousands of concurrent bots, those efficiency gains translate into measurable cost reductions on Cloudflare’s pay‑as‑you‑go pricing model. Early adopters report being able to double the number of active agents on the same budget, while also seeing a 15% drop in page‑load failures caused by rate‑limiting or CAPTCHAs, thanks to the platform’s built‑in DDoS mitigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implications for AI Development and the Browser Landscape
&lt;/h2&gt;

&lt;p&gt;Kitesurf arrives at a moment when generative AI is being woven into everyday workflows. Companies are experimenting with agents that can browse the web, summarize articles, or even negotiate contracts in real time. Until now, the bottleneck has often been the browser layer—developers must balance fidelity against cost, and many resort to brittle scraping techniques.&lt;/p&gt;

&lt;p&gt;By offering a purpose‑built, cloud‑scalable browser, Cloudflare lowers the entry barrier for building reliable AI agents. The platform’s API also integrates with popular model hosting services, allowing developers to chain language model inference with live web interaction in a single pipeline. This could accelerate use cases such as autonomous market research, real‑time compliance monitoring, and personalized shopping assistants.&lt;/p&gt;

&lt;p&gt;Moreover, Kitesurf signals a shift in how the industry views browsers: no longer just a human interface but also a programmable compute substrate. If the model gains traction, we may see a new ecosystem of AI‑first extensions, security policies, and pricing tiers that treat browser time as a first‑class cloud resource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Cloudflare’s Kitesurf reimagines the web browser as a lean, cloud‑native engine for AI agents, delivering lower compute costs and higher scalability than traditional headless Chromium. For developers building autonomous web bots, the service could become a fast‑track to production‑grade performance without the overhead of managing complex infrastructure.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>ai</category>
      <category>browsers</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Top 5 Cloudflare AI Gateway Alternatives for Enterprises in 2026</title>
      <dc:creator>Kuldeep Paul</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:25:29 +0000</pubDate>
      <link>https://dev.to/kuldeep_paul/top-5-cloudflare-ai-gateway-alternatives-for-enterprises-in-2026-40bj</link>
      <guid>https://dev.to/kuldeep_paul/top-5-cloudflare-ai-gateway-alternatives-for-enterprises-in-2026-40bj</guid>
      <description>&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%2Fq1lw8cp1ojlw7az0l3yr.jpg" 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%2Fq1lw8cp1ojlw7az0l3yr.jpg" alt="Top 5 Cloudflare AI Gateway Alternatives for Enterprises in 2026" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An analysis of the leading Cloudflare AI Gateway alternatives in 2026, comparing &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; against other top enterprise tools on latency, security, and VPC controls.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Routing high-concurrency LLM traffic through a public, managed-only proxy can introduce unnecessary network latency, data perimeter risks, and unpredicted platform costs. While Cloudflare AI Gateway offers convenient edge-based caching, many enterprise teams require greater deployment flexibility and deeper, self-hosted governance. &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;, an &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; written in Go by Maxim AI, is designed to solve these infrastructure challenges by running natively inside an organization's private network. This article evaluates the top five Cloudflare AI Gateway alternatives for enterprises in 2026, comparing their architecture, performance, and security controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Enterprises Seek Cloudflare AI Gateway Alternatives
&lt;/h2&gt;

&lt;p&gt;Cloudflare AI Gateway is a SaaS, closed-source product. While its integration with Cloudflare's global edge CDN makes it appealing for small-scale projects, enterprise infrastructure teams often encounter roadblocks when scaling production workloads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lack of VPC and Air-Gapped Deployment:&lt;/strong&gt; Cloudflare is a managed service. All prompts, system messages, and model completions must leave the enterprise network perimeter and traverse Cloudflare's public infrastructure. For companies operating under strict regulatory frameworks (such as HIPAA, GDPR, or SOC 2), this external data routing introduces critical compliance and security risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Hop Latency:&lt;/strong&gt; Public proxies add a network transit hop between the application server, the gateway, and the LLM provider. In multi-step agentic systems where a single user interaction triggers consecutive model calls, this added edge latency degrades the user experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granular, Hierarchical Budgeting:&lt;/strong&gt; Cloudflare has introduced spend and rate limits, but it lacks the capability to delegate, partition, and track dynamic budgets across hundreds of internal teams, external clients, or individual developer keys from a single control plane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic Tool Integration (Model Context Protocol):&lt;/strong&gt; Modern enterprise AI applications use the Model Context Protocol (MCP) to connect LLMs to local databases, file systems, and internal APIs. Cloudflare is focused primarily on traditional model APIs, leaving a security and governance gap for multi-agent tool execution.&lt;/li&gt;
&lt;/ul&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%2Fb653fuwcvk1hy255f7nr.jpg" 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%2Fb653fuwcvk1hy255f7nr.jpg" alt="An illustration of split pathways, with one red pathway sending glowing data spheres out of a network bubble into a publ" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Top 5 Cloudflare AI Gateway Alternatives
&lt;/h2&gt;

&lt;p&gt;The following five alternatives represent the leading enterprise-grade AI gateways in 2026, prioritized by performance, compliance, and deployment flexibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Bifrost: Best for Performance, VPC Isolation, and Agentic Workflows
&lt;/h3&gt;

&lt;p&gt;When routing mission-critical workloads, &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; represents the standard for performance and network control. Bifrost, an Apache-2.0 licensed, Go-based gateway, functions as a high-performance proxy in front of &lt;a href="https://docs.getbifrost.ai/providers/supported-providers/overview" rel="noopener noreferrer"&gt;20+ model providers&lt;/a&gt;. It integrates seamlessly with existing codebases as a &lt;a href="https://docs.getbifrost.ai/features/drop-in-replacement" rel="noopener noreferrer"&gt;drop-in replacement&lt;/a&gt; for major SDKs, requiring developers to adjust only their base URL parameters.&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="c1"&gt;# Example Bifrost configuration for multi-provider routing and fallbacks&lt;/span&gt;
&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai-primary&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${OPENAI_API_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anthropic-backup&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anthropic&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ANTHROPIC_API_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

&lt;span class="na"&gt;fallback_chain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai-primary&lt;/span&gt;
    &lt;span class="na"&gt;failover&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anthropic-backup&lt;/span&gt;
    &lt;span class="na"&gt;on_codes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;429&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;500&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;503&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bifrost is engineered specifically for high throughput. It introduces just &lt;a href="https://docs.getbifrost.ai/benchmarking/getting-started" rel="noopener noreferrer"&gt;11 microseconds of overhead&lt;/a&gt; per request at 5,000 requests per second under sustained load. These metrics are verified by public &lt;a href="https://www.getmaxim.ai/bifrost/resources/benchmarks" rel="noopener noreferrer"&gt;performance benchmarks&lt;/a&gt;, showing that it maintains sub-millisecond latency profiles where Python-based proxies run into execution bottlenecks.&lt;/p&gt;

&lt;p&gt;To minimize model token spend, Bifrost implements &lt;a href="https://docs.getbifrost.ai/features/semantic-caching" rel="noopener noreferrer"&gt;semantic caching&lt;/a&gt;. This system matches incoming prompts against a vector store of historical requests, allowing the gateway to instantly replay cached completions for highly similar queries instead of routing them to expensive upstream models.&lt;/p&gt;

&lt;p&gt;Furthermore, Bifrost utilizes &lt;a href="https://docs.getbifrost.ai/features/governance/virtual-keys" rel="noopener noreferrer"&gt;virtual keys&lt;/a&gt; as its core cost-control abstraction. These virtual credentials enforce hierarchical rate limits and cost caps across individual consumers, teams, and departments. On the agentic front, its built-in &lt;a href="https://www.getmaxim.ai/bifrost/resources/mcp-gateway" rel="noopener noreferrer"&gt;Model Context Protocol (MCP) gateway&lt;/a&gt; supports a dynamic Code Mode, which lets LLMs orchestrate multiple tools locally, resulting in up to 50% fewer tokens and 40% lower execution latency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Extremely low latency (11 microseconds under load); full in-VPC or air-gapped deployment; granular hierarchical budgets; native MCP client and server integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Requires operations capacity to self-host, although a managed enterprise option is available for teams wanting a zero-ops control plane.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. LiteLLM: Best for Developer Compatibility and Fast Prototyping
&lt;/h3&gt;

&lt;p&gt;For small-scale projects or fast prototyping, &lt;a href="https://www.litellm.ai/" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt; is a widely used open-source Python proxy. It normalizes input and output schemas across 140+ upstream model providers, allowing developers to switch between various models using a single OpenAI-compatible SDK format.&lt;/p&gt;

&lt;p&gt;LiteLLM is packaged as a Docker container, making it easy to deploy on standard cloud compute instances. It includes database-backed rate limiting, basic team tracking dashboards, and native Slack alert integrations for key failures.&lt;/p&gt;

&lt;p&gt;However, because LiteLLM is built on Python's async framework, it introduces significant runtime latency and CPU overhead under high concurrency. In sustained enterprise environments where hundreds of requests execute simultaneously, this processing overhead can cause latency to spike, making it less suitable for performance-critical agent chains.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Broad multi-model normalizations; easy to deploy via Docker; active open-source community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Higher latency overhead compared to compiled Go gateways; advanced enterprise features (OIDC, audit logs) are locked behind its commercial tier; Python runtime introduces concurrency limitations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Kong AI Gateway: Best for Teams Already Running the Kong Mesh
&lt;/h3&gt;

&lt;p&gt;Enterprises that have already consolidated their web API traffic on the Kong API platform often use &lt;a href="https://konghq.com/products/kong-ai-gateway" rel="noopener noreferrer"&gt;Kong AI Gateway&lt;/a&gt; to manage their LLM endpoints. Rather than deploying a separate proxy, this solution runs as a suite of AI-specific plugins layered onto Kong's legacy Nginx/Lua-based core.&lt;/p&gt;

&lt;p&gt;Kong's architecture uses plugins to handle standard API tasks, such as token-based rate limiting, OAuth authentication, and route transformations. Its AI-specific plugins add prompt injection protection, PII redaction, and multi-provider load balancing.&lt;/p&gt;

&lt;p&gt;While Kong is highly stable, if your organization does not already use the broader Kong ecosystem, deploying it purely for LLM traffic adds unnecessary configuration and infrastructure overhead. Furthermore, because it was designed for standard REST APIs, it lacks native AI-native tools like interactive prompt playgrounds, automated evaluation datasets, or Model Context Protocol orchestration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Reuses existing web API security, routing, and access control patterns; extensive plugin catalog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Heavy operational footprint for greenfield setups; Lua runtime configuration can be complex; lacks deep AI-native lifecycle features.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. OpenRouter: Best for Zero-Ops Managed Multi-Model Access
&lt;/h3&gt;

&lt;p&gt;For teams that prioritize ease of use and zero infrastructure management over data perimeter control, &lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; represents a compelling managed alternative. It acts as an API aggregator and model marketplace, providing a single endpoint for hundreds of commercial and open-weight models.&lt;/p&gt;

&lt;p&gt;OpenRouter manages the upstream API keys and offers a unified billing system, letting developers test and switch between models (such as Claude, Llama, and Mistral) without maintaining accounts with individual model providers.&lt;/p&gt;

&lt;p&gt;The main drawback for enterprise buyers is security and compliance. Since OpenRouter is a managed SaaS, every prompt is forwarded through their public edge nodes. This setup prevents self-hosted deployment inside an isolated VPC, which is a common requirement for enterprises handling proprietary source code or confidential client data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Completely managed; unified billing; access to hundreds of open-source and closed-source models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Data leaves the corporate boundary; added network hops increase total latency; no on-premise governance or local vector caching.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5. Azure API Management (GenAI Policies): Best for Azure-Native Architectures
&lt;/h3&gt;

&lt;p&gt;Large enterprises whose infrastructure is entirely committed to the Microsoft cloud often route their AI traffic using &lt;a href="https://azure.microsoft.com/en-us/products/api-management/" rel="noopener noreferrer"&gt;Azure API Management&lt;/a&gt;. Microsoft has introduced specialized generative AI policies to help cloud architects manage, secure, and monitor their enterprise LLM endpoints.&lt;/p&gt;

&lt;p&gt;These native policies handle token-based rate limiting (such as throttling based on Tokens Per Minute), circuit breaking across regional Azure OpenAI deployments, and secure credential storage within Azure Key Vault.&lt;/p&gt;

&lt;p&gt;Azure's solution excels in high-security, Azure-only cloud architectures. However, routing traffic to non-Azure models (like Anthropic or locally hosted open-weight models) is highly manual, and the platform lacks the native semantic caching and developer-friendly playground tools found in dedicated AI gateways.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros:&lt;/strong&gt; Native enterprise compliance inside Azure; robust token rate-limiting; seamless Entra ID and Key Vault integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Highly complex multi-cloud configurations; heavily optimized for Azure OpenAI at the expense of other model providers; no native tool or agent orchestration capabilities.&lt;/li&gt;
&lt;/ul&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%2Fs8vq41tqfmfxi1kqiucm.jpg" 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%2Fs8vq41tqfmfxi1kqiucm.jpg" alt="A clean, modern visual comparison of five distinct colored server columns standing next to each other, with the first co" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparing Alternatives on Key Enterprise Dimensions
&lt;/h2&gt;

&lt;p&gt;The table below outlines how the leading Cloudflare AI Gateway alternatives compare across the architectural criteria most critical to enterprise platform teams.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;&lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;LiteLLM&lt;/th&gt;
&lt;th&gt;Kong AI Gateway&lt;/th&gt;
&lt;th&gt;OpenRouter&lt;/th&gt;
&lt;th&gt;Azure API Management&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sub-millisecond (11µs P99)&lt;/td&gt;
&lt;td&gt;High (Python asyncio bottleneck)&lt;/td&gt;
&lt;td&gt;Moderate (Lua/Nginx pipeline)&lt;/td&gt;
&lt;td&gt;High (Public SaaS edge hops)&lt;/td&gt;
&lt;td&gt;Moderate (Cloud gateway routing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;VPC / On-Premise / Hybrid&lt;/td&gt;
&lt;td&gt;VPC / On-Premise&lt;/td&gt;
&lt;td&gt;Hybrid / On-Premise&lt;/td&gt;
&lt;td&gt;Managed SaaS Only&lt;/td&gt;
&lt;td&gt;Managed Cloud (Azure-bound)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hierarchical Budgeting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep (Virtual Key, Team, Client)&lt;/td&gt;
&lt;td&gt;Basic (API Key only)&lt;/td&gt;
&lt;td&gt;Basic (Consumer limits)&lt;/td&gt;
&lt;td&gt;Basic (Account limits)&lt;/td&gt;
&lt;td&gt;Basic (Subscription limits)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP &amp;amp; Agent Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native (Client &amp;amp; Server Gateway)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Basic (Custom Lua plugins)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Optimization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.getbifrost.ai/features/semantic-caching" rel="noopener noreferrer"&gt;Semantic Caching&lt;/a&gt; &amp;amp; Code Mode&lt;/td&gt;
&lt;td&gt;Basic Caching&lt;/td&gt;
&lt;td&gt;Basic Caching Plugins&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Enterprise AI Security: Extending Gateway Controls to the Endpoint
&lt;/h2&gt;

&lt;p&gt;Deploying a server-side gateway is only the first phase of a comprehensive enterprise AI security strategy. While backend application traffic can be routed through a centralized corporate endpoint, employees routinely download local desktop applications, access web-based LLM chat portals, or configure terminal-based coding agents on their corporate laptops, creating a major shadow AI compliance blind spot.&lt;/p&gt;

&lt;p&gt;Beyond centralized gateway routing, &lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; applies robust server-side &lt;a href="https://www.getmaxim.ai/bifrost/resources/governance" rel="noopener noreferrer"&gt;governance&lt;/a&gt; and security controls, such as virtual keys, budget limits, and immutable &lt;a href="https://docs.getbifrost.ai/enterprise/audit-logs" rel="noopener noreferrer"&gt;audit logs&lt;/a&gt;. To extend these exact policies to individual employee workstations, &lt;a href="https://www.getmaxim.ai/bifrost/edge" rel="noopener noreferrer"&gt;Bifrost Edge&lt;/a&gt; (currently in alpha) operates as an endpoint governance agent that runs natively on macOS, Windows, and Linux. This combined gateway and endpoint architecture automatically redirects all local developer AI traffic through the enterprise's central policy layer without requiring any manual reconfiguration of client applications.&lt;/p&gt;

&lt;p&gt;Using the Bifrost Edge agent, security and platform teams can enforce &lt;a href="https://docs.getbifrost.ai/edge/app-governance" rel="noopener noreferrer"&gt;app governance&lt;/a&gt; to permit only approved desktop tools, map and block unverified third-party scripts via &lt;a href="https://docs.getbifrost.ai/edge/mcp-governance" rel="noopener noreferrer"&gt;MCP governance&lt;/a&gt;, and enforce data loss prevention (DLP) rules using native &lt;a href="https://docs.getbifrost.ai/edge/security" rel="noopener noreferrer"&gt;endpoint security&lt;/a&gt; patterns. This ensures that sensitive corporate data, raw intellectual property, and hardcoded database credentials never leave the laptop, maintaining a secure data perimeter across the entire enterprise fleet.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Choose the Right AI Gateway for Your Enterprise
&lt;/h2&gt;

&lt;p&gt;Selecting the ideal gateway involves evaluating your performance requirements, existing architecture, and compliance boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose Bifrost&lt;/strong&gt; if your applications require sub-millisecond routing, deployment inside a private VPC or air-gapped network, semantic caching to lower token costs, or advanced Model Context Protocol (MCP) tool governance across both backend servers and employee endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose LiteLLM&lt;/strong&gt; if your team is standardizing on Python-based infrastructure, is running low-concurrency prototype workloads, and does not require enterprise-grade low-latency performance under load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose Kong AI Gateway&lt;/strong&gt; if your IT infrastructure is already operating on the Kong API mesh, allowing you to easily reuse existing API management, monitoring, and authentication plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose Azure API Management&lt;/strong&gt; if your application architecture is fully committed to the Microsoft Azure tenant and your LLM usage is exclusively focused on Azure OpenAI instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose OpenRouter&lt;/strong&gt; if you are building an early-stage prototype, need immediate access to hundreds of public models, and do not need to enforce strict on-premise data governance or network isolation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineering and platform teams evaluating Cloudflare AI Gateway alternatives can explore the &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;Bifrost GitHub repository&lt;/a&gt; to inspect the open-source code, or &lt;a href="https://getmaxim.ai/bifrost/book-a-demo" rel="noopener noreferrer"&gt;request a Bifrost demo&lt;/a&gt; to see how native endpoint AI governance and high-performance routing operate at enterprise scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/" rel="noopener noreferrer"&gt;Azure API Management Generative AI Policies Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/ai-gateway/" rel="noopener noreferrer"&gt;Cloudflare AI Gateway Docs: Spend Limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.konghq.com/gateway/latest/ai/" rel="noopener noreferrer"&gt;Kong AI Gateway Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.getbifrost.ai/benchmarking/getting-started" rel="noopener noreferrer"&gt;Bifrost Performance Benchmarks Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cloudflare</category>
      <category>aigateway</category>
      <category>enterprise</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Free AI Image Prompt Gallery + Browser-Based Image Toolkit — Here's What I Learned</title>
      <dc:creator>Wenjie Zhang</dc:creator>
      <pubDate>Thu, 20 Aug 2026 00:19:34 +0000</pubDate>
      <link>https://dev.to/wenjie_zhang_6e56b775216c/i-built-a-free-ai-image-prompt-gallery-browser-based-image-toolkit-heres-what-i-learned-551i</link>
      <guid>https://dev.to/wenjie_zhang_6e56b775216c/i-built-a-free-ai-image-prompt-gallery-browser-based-image-toolkit-heres-what-i-learned-551i</guid>
      <description>&lt;p&gt;If you've ever spent twenty minutes hunting for the right prompt to get Nano Banana or GPT Image to output &lt;em&gt;exactly&lt;/em&gt; the poster style you had in mind, this post — and the tool I built — is for you.&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%2Fg5da3jppp6j3w1t8k2ch.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%2Fg5da3jppp6j3w1t8k2ch.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recently launched &lt;strong&gt;&lt;a href="https://hereimg.com" rel="noopener noreferrer"&gt;HereImg&lt;/a&gt;&lt;/strong&gt;, a site that combines two things I personally kept needing and never found bundled together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A searchable &lt;strong&gt;gallery of AI image prompts&lt;/strong&gt; (organized by model and category, not just a random Twitter thread)&lt;/li&gt;
&lt;li&gt;A set of &lt;strong&gt;fast, free, browser-based image editing tools&lt;/strong&gt; for the boring-but-essential stuff — resize, crop, compress, remove background, etc.&lt;/li&gt;
&lt;/ol&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%2Fj365whfv7u7wxvtlfr1v.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%2Fj365whfv7u7wxvtlfr1v.png" alt=" " width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's why I built it, what's under the hood, and how it might save you some time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: prompt engineering for images is still trial-and-error
&lt;/h2&gt;

&lt;p&gt;Text-to-image models like &lt;strong&gt;Nano Banana&lt;/strong&gt;, &lt;strong&gt;GPT Image&lt;/strong&gt;, and &lt;strong&gt;Seedream&lt;/strong&gt; are incredible, but getting consistent, high-quality output still comes down to prompt phrasing. Most people either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scroll through scattered social media posts hoping to find a prompt that matches their use case, or&lt;/li&gt;
&lt;li&gt;Burn through credits re-rolling generations with slightly different wording&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a place where prompts are organized like a real reference library — by &lt;strong&gt;model&lt;/strong&gt; (Nano Banana / GPT Image / Seedream) and by &lt;strong&gt;category&lt;/strong&gt; (Food &amp;amp; Drink, Illustration &amp;amp; 3D, Photography, Poster Design, Product &amp;amp; Brand, UI &amp;amp; Graphic) — so you can browse examples close to what you're trying to make, copy a working prompt, and tweak it instead of starting from zero.&lt;/p&gt;

&lt;p&gt;Every prompt in the gallery is paired with its actual generated output, so you're not guessing what a phrase like "cinematic rim lighting, 35mm" will actually produce.&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%2F253motap0hlq3m4op5x3.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%2F253motap0hlq3m4op5x3.png" alt=" " width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: tools you need &lt;em&gt;before&lt;/em&gt; and &lt;em&gt;after&lt;/em&gt; generating
&lt;/h2&gt;

&lt;p&gt;AI generation is rarely the last step. You still need to resize the output for Instagram, crop it for a thumbnail, strip the background for a product shot, or compress it before uploading. So I bundled a &lt;strong&gt;Magic Tools&lt;/strong&gt; suite alongside the prompt gallery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Social Media Resizer (pre-set dimensions for major platforms)&lt;/li&gt;
&lt;li&gt;Image Cropper / Resizer / Rotator &amp;amp; Flip&lt;/li&gt;
&lt;li&gt;Compressor &amp;amp; Format Converter (JPG/PNG/WebP)&lt;/li&gt;
&lt;li&gt;Brightness &amp;amp; Contrast, Filters, Grayscale&lt;/li&gt;
&lt;li&gt;Color Picker (extract hex codes / palettes)&lt;/li&gt;
&lt;li&gt;Watermark tool&lt;/li&gt;
&lt;li&gt;AI Background Remover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design decision: &lt;strong&gt;all the "free tools" run entirely client-side in the browser.&lt;/strong&gt; Nothing gets uploaded to a server. That means no signup, no file size anxiety, no waiting on an upload queue — and your images never leave your device. Only the AI-powered features (like background removal) touch a backend, and those are clearly marked with a credit cost.&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%2F70tmqa3b7m5ly09yfmjw.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%2F70tmqa3b7m5ly09yfmjw.png" alt=" " width="799" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;For fellow builders curious about the stack: HereImg runs on &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;, which has been a great fit for this kind of product —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge deployment means the tool feels instant no matter where in the world someone opens it&lt;/li&gt;
&lt;li&gt;Cheap to run at scale, which matters a lot as an indie/bootstrapped project&lt;/li&gt;
&lt;li&gt;Pairs naturally with client-side image processing, since the heavy lifting (canvas/WASM operations) happens in the visitor's browser rather than on my infra&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's interest, I can write a follow-up post specifically on the Workers + D1 setup, the KV caching strategy for the prompt gallery, and some of the deployment gotchas I hit along the way (happy to share the pain so you don't repeat it).&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%2F0t9qj9qm81po4ynt7gc3.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%2F0t9qj9qm81po4ynt7gc3.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;🖼️ &lt;a href="https://hereimg.com" rel="noopener noreferrer"&gt;Browse the prompt gallery&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🛠️ &lt;a href="https://hereimg.com/tools" rel="noopener noreferrer"&gt;Magic Tools suite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎁 New accounts get &lt;strong&gt;20 free credits&lt;/strong&gt; to try AI generation/background removal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's free to use, no login required for the browser-based tools, and I'd genuinely love feedback from this community — especially if you spot a prompt category I'm missing or a tool you wish existed.&lt;/p&gt;

&lt;p&gt;If you build something with it (or find a prompt that works great), drop it in the comments — I'll add good ones to the gallery.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>cloudflare</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Cloudflare OS Launches Open Platform for AI Agents</title>
      <dc:creator>Felipe L</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:27:30 +0000</pubDate>
      <link>https://dev.to/felipejac/cloudflare-os-launches-open-platform-for-ai-agents-bak</link>
      <guid>https://dev.to/felipejac/cloudflare-os-launches-open-platform-for-ai-agents-bak</guid>
      <description>&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;Cloudflare released Cloudflare OS, an open platform for building AI agents, applications, and automated workflows. The OS bundles services, APIs, and tooling that let developers launch agents anywhere from the edge to the cloud, with built‑in scaling, security, and observability.&lt;/p&gt;

&lt;p&gt;Cloudflare OS delivers a unified runtime, a marketplace for reusable components, and integration hooks that connect agents to external services, databases, and other agents. The code and SDKs are open source on GitHub, and the platform works with existing workflow engines like &lt;a href="https://dev.to/go/n8n"&gt;n8n&lt;/a&gt;, &lt;a href="https://dev.to/go/zapier"&gt;Zapier&lt;/a&gt;, and custom orchestration frameworks.&lt;/p&gt;

&lt;p&gt;The launch aligns with the industry’s push toward edge‑first AI, where latency and privacy matter. By providing a managed runtime that runs near users, Cloudflare OS cuts round‑trip times for agent‑driven tasks while keeping data on the edge whenever possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Builders
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalable Edge Runtime&lt;/strong&gt;: Deploy n8n workflows or custom AI agents directly to Cloudflare’s edge network. Reduce latency for time‑sensitive tasks such as real‑time data enrichment or user‑specific personalization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified API Surface&lt;/strong&gt;: A consistent set of APIs for authentication, storage, and messaging cuts the friction of integrating multiple third‑party services. Teams can focus on business logic instead of plumbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace for Components&lt;/strong&gt;: A curated library of pre‑built agent modules—NLP, data transformation, and more—can be plugged into existing workflows. This speeds prototyping and lowers the barrier to adding AI capabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability &amp;amp; Security&lt;/strong&gt;: Built‑in logging, tracing, and fine‑grained access controls let production‑grade workflows be monitored and audited out of the box. This is a major advantage in compliance‑heavy environments compared to generic cloud VMs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open‑Source Flexibility&lt;/strong&gt;: The core is open source. Teams can fork, extend, or embed the runtime into their own infrastructure for hybrid or on‑prem deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A team that already uses n8n can deploy a new agent on Cloudflare OS for low‑latency transformations, import a community‑built NLP component from the marketplace, chain it with existing tasks, and rely on Cloudflare’s observability stack to monitor performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I run my existing n8n workflows on Cloudflare OS without rewriting them?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Yes. Cloudflare OS includes a runtime wrapper that accepts n8n’s JSON workflow definitions and executes them in the edge environment. Minimal configuration is needed to connect to your data sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Cloudflare OS support custom AI models, or only pre‑built ones?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: You can host your own model containers or call external model endpoints. The marketplace offers pre‑built models for common tasks, but you can also deploy your own models to the edge or cloud as needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What about cost and scaling?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Cloudflare’s edge network charges per request and per GB of data processed. For high‑volume, low‑latency workloads, this can be cheaper than running dedicated VMs. The OS automatically scales agent instances based on traffic, so you pay only for what you use.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://automationscookbook.com/blog/cloudflare-os-launches-open-platform-for-ai-agents-20260806" rel="noopener noreferrer"&gt;Automations Cookbook&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>edgecomputing</category>
      <category>aiagents</category>
      <category>automation</category>
    </item>
    <item>
      <title>Traffic Anomaly or Outage? What a 30% Drop Actually Means</title>
      <dc:creator>Umesh Malik</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:05:31 +0000</pubDate>
      <link>https://dev.to/umesh_malik/traffic-anomaly-or-outage-what-a-30-drop-actually-means-dd</link>
      <guid>https://dev.to/umesh_malik/traffic-anomaly-or-outage-what-a-30-drop-actually-means-dd</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traffic anomaly or outage?&lt;/strong&gt; The question every on-call engineer dreads has a mechanical answer: compare the drop against a baseline matched by time-of-day and day-of-week, then check whether the deviation's timing lines up with an independent, verifiable cause. Cloudflare answered it in minutes for the August 12, 2026 solar eclipse — traffic fell &lt;strong&gt;15-30%&lt;/strong&gt; along the path of totality, and as much as &lt;strong&gt;-46.7%&lt;/strong&gt; in the hardest-hit countries — by comparing five-minute traffic buckets against the median of the same slot on the three preceding Wednesdays, then confirming the drop's shape matched the eclipse's geometrically computed timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened during the eclipse
&lt;/h2&gt;

&lt;p&gt;On August 12, 2026, a total solar eclipse crossed a path starting near Alaska around 15:35 UTC and sweeping through the North Atlantic into Europe. &lt;a href="https://blog.cloudflare.com/total-eclipse-internet-traffic-iceland-spain-portugal/" rel="noopener noreferrer"&gt;Cloudflare's Radar team documented the traffic impact&lt;/a&gt; as the moon's shadow moved: "regions along the path of totality saw traffic fall by roughly 15% to 30%" at the moment of maximum obscuration.&lt;/p&gt;

&lt;p&gt;Individual countries moved even more — the full range across affected regions was "9.3 to -46.7%" — with Iceland, Spain, and Portugal registering the steepest declines, alongside smaller drops in Ireland, the UK, and France. Countries with only shallow partial coverage, like Sweden, Denmark, Poland, and Switzerland, showed close to nothing. That's a bigger swing than most teams would tolerate silently: a 30-46% drop in request volume, in a five-to-twenty-minute window, is the kind of number that pages an on-call engineer at 3 a.m.&lt;/p&gt;

&lt;p&gt;The interesting part isn't that traffic dropped — outdoor daylight events pulling people off their phones is intuitive. The interesting part is how Cloudflare proved it was the eclipse and not a routing problem, a DNS issue, or a partial regional outage, all of which produce the exact same symptom: a sudden request-volume cliff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traffic anomaly or outage: the method that actually decides it
&lt;/h2&gt;

&lt;p&gt;Cloudflare's stated methodology was specific: "We compared eclipse day against the same weekday: the median of the three previous Wednesdays, matched slot-by-slot on time-of-day." Traffic was bucketed into five-minute windows, and each window on eclipse day was checked against the median of that identical five-minute window on the prior three Wednesdays — not against yesterday, not against a rolling daily average, and not against a single reference day.&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%2Fk9i4x27aga9sx150p48h.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%2Fk9i4x27aga9sx150p48h.png" alt="Flow diagram showing raw five-minute traffic buckets compared against a median baseline from three prior same-weekday, same-time-slot windows, producing a deviation signal that is checked against a known event's timing before being classified as anomaly or outage" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then came the step that actually establishes causation rather than mere correlation: they checked the &lt;em&gt;timing&lt;/em&gt;. The obscuration percentage at each location was computed geometrically from real sun and moon positions, and the traffic dip's shape — onset, trough, and recovery — was checked against that computed obscuration curve at each location.&lt;/p&gt;

&lt;p&gt;Cloudflare's own framing: "Paired with the precise timing of the drops, the trend of the data demonstrates that the eclipse itself was the primary driver of the decline." A drop that lines up minute-for-minute with an independently computable physical event is not the same kind of evidence as a drop that merely happened during the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naive baselines lie
&lt;/h2&gt;

&lt;p&gt;Most homegrown anomaly detectors compare against something too crude to survive contact with real-world variance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Naive baseline&lt;/th&gt;
&lt;th&gt;What breaks it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Compare to yesterday"&lt;/td&gt;
&lt;td&gt;Day-of-week effects (Wednesday ≠ Tuesday) dwarf small real signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Compare to a flat daily average"&lt;/td&gt;
&lt;td&gt;Erases the intraday shape — traffic isn't flat across 24 hours, so every trough looks anomalous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Compare to last week, same day"&lt;/td&gt;
&lt;td&gt;One sample is still noisy; a single unusual prior week poisons every comparison after it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Alert on any % deviation"&lt;/td&gt;
&lt;td&gt;No mechanism to separate a real external cause from ordinary variance, so every threshold is a guess&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare's baseline avoids all four failure modes at once. Matching by time-of-day removes the intraday shape. Matching by weekday removes the day-of-week effect. Taking the &lt;em&gt;median&lt;/em&gt; of three weeks, not one, removes single-week noise without being distorted by one outlier week the way a mean would be.&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%2Fi34j2jnq751a64u1nngh.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%2Fi34j2jnq751a64u1nngh.png" alt="Comparison chart contrasting a naive single-prior-day baseline, which shows a false-positive-prone jagged deviation line, against a three-week same-slot median baseline, which shows a smooth deviation line that only spikes during the real eclipse window" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building this yourself: a baseline that doesn't cry wolf
&lt;/h2&gt;

&lt;p&gt;You don't need Cloudflare's edge network to apply the same logic to your own traffic, request-rate, or error-rate monitoring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bucket at the resolution of the event you care about.&lt;/strong&gt; Five minutes is a solid default for web traffic — coarse enough to smooth per-request noise, fine enough to catch anything that resolves within 15-20 minutes. If your real events are shorter, tighten the bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match by time-of-day and day-of-week, not just by date.&lt;/strong&gt; A Wednesday-at-3pm bucket should be compared to prior Wednesdays at 3pm, never to Tuesday at 3pm or to Wednesday at 9am.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take the median across 3+ prior matched windows, not the mean of one or two.&lt;/strong&gt; The median is robust to a single unusual prior week (an outage, a holiday, a promo) in a way a two-sample average never is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require a second, independent signal before calling it a real anomaly.&lt;/strong&gt; Cloudflare had the eclipse's geometric timing. You might have a deploy timestamp, a known maintenance window, a marketing campaign's send time, or a correlated metric (CPU, upstream latency) moving in the same shape. Deviation alone is a symptom; deviation &lt;em&gt;plus&lt;/em&gt; a matching independent cause is a diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only page on deviation without an independent cause.&lt;/strong&gt; If the drop matches nothing you can explain, that's exactly the case worth waking someone up for — you've now filtered out the explainable noise, so what's left is genuinely worth investigating.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;The single biggest mistake is treating "deviation from baseline" and "root cause identified" as the same finding. They aren't. A baseline tells you &lt;em&gt;that&lt;/em&gt; something changed; it never tells you &lt;em&gt;why&lt;/em&gt;. Teams that stop at step 1-3 above end up with dashboards that are technically correct and practically useless — every real anomaly looks identical to a genuine incident until someone manually checks for an external cause, which is exactly the step Cloudflare didn't skip.&lt;/p&gt;

&lt;p&gt;The second mistake is picking a bucket size and baseline window once and never revisiting them as traffic patterns shift — a baseline tuned for pre-pandemic office-hours traffic, for instance, will misfire constantly against post-2020 traffic shapes. Re-validate your baseline's assumptions whenever you meaningfully change what "normal" looks like for your service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A 15-30% traffic drop is not, by itself, evidence of anything. It's the shape of the drop against a properly matched baseline, plus an independent explanation for its exact timing, that turns a scary number into either "expected, stand down" or "genuinely worth paging someone." Cloudflare had a solar eclipse as its independent signal. Most teams have deploy logs, feature-flag timestamps, and marketing calendars sitting right there, unused, the next time a dashboard spikes red.&lt;/p&gt;

&lt;p&gt;If you're building alerting on top of Cloudflare Workers, see how &lt;a href="https://umesh-malik.com/blog/cloudflare-access-for-workers" rel="noopener noreferrer"&gt;Cloudflare Access&lt;/a&gt; and &lt;a href="https://umesh-malik.com/blog/remove-cloudflare-beacon-min-js" rel="noopener noreferrer"&gt;removing unnecessary beacon scripts&lt;/a&gt; affect what you're even measuring. For the discipline of tracing a symptom back to a verified root cause, the same rigor shows up in &lt;a href="https://umesh-malik.com/blog/fix-eevdf-latency-regression-sched-ext" rel="noopener noreferrer"&gt;debugging an EEVDF latency regression&lt;/a&gt; and in &lt;a href="https://umesh-malik.com/blog/zero-downtime-database-migration-dual-writes" rel="noopener noreferrer"&gt;running a zero-downtime dual-write migration&lt;/a&gt; — both are exercises in not trusting the first plausible explanation. And if your traffic anomaly turns out to be automated rather than seasonal, &lt;a href="https://umesh-malik.com/blog/verify-ai-crawler-ips-not-user-agents" rel="noopener noreferrer"&gt;verifying crawler IPs instead of trusting User-Agent strings&lt;/a&gt; is the next place to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you tell a real traffic anomaly from an outage?&lt;/strong&gt; You compare the drop against a baseline built from the same time-of-day and day-of-week, not against yesterday or a flat average. If the deviation lines up precisely with a known external cause — a scheduled event, a regional pattern, a correlated signal — and recovers on the same timeline that cause predicts, it's an anomaly, not an outage. An outage has no such external correlate and typically doesn't self-correct on a predictable schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is comparing traffic to yesterday a bad baseline?&lt;/strong&gt; Day-of-week effects are large enough to swamp small real signals — enterprise traffic on a Wednesday can differ from a Tuesday by more than the anomaly you're trying to detect. A single prior day is also one noisy sample, so any randomness in that one day becomes randomness in your alert threshold. A multi-week, same-weekday, same-time-slot median averages that noise out before you ever compare it to today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Cloudflare Radar and what did it measure for the 2026 eclipse?&lt;/strong&gt; Cloudflare Radar is Cloudflare's public internet-traffic-and-trends dashboard, drawing on request data from its global edge network. For the August 12, 2026 total solar eclipse, Radar's team compared HTTP request volume in five-minute buckets against a baseline built from the three preceding Wednesdays at matching times of day, and found traffic changes ranging from +9.3% to -46.7% across affected countries, with the deepest drops in Iceland, Spain, and Portugal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What bucket size should I use for anomaly detection?&lt;/strong&gt; Five minutes is a reasonable default for web traffic: coarse enough to smooth out per-request jitter, fine enough to catch a drop that starts and ends within 15-20 minutes. Match your bucket size to the shortest real event you care about — if you need to catch something that resolves in under five minutes, bucket at one or two minutes instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does differential privacy or noise correction matter for this kind of baseline?&lt;/strong&gt; Not for this specific case — aggregate HTTP request counts across a whole country aren't sensitive per-user data, so Cloudflare's eclipse analysis didn't need privacy-preserving noise. That technique matters for a different problem: when the metric itself could reveal something about an individual, which is a concern in on-device telemetry pipelines, not in coarse regional traffic monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare, &lt;a href="https://blog.cloudflare.com/total-eclipse-internet-traffic-iceland-spain-portugal/" rel="noopener noreferrer"&gt;"Total eclipse of the Internet: traffic impacts in Iceland, Spain, and Portugal"&lt;/a&gt; — the traffic percentages, methodology, and timing correlation cited throughout.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://radar.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Radar&lt;/a&gt; — the public dashboard the analysis draws from, for readers who want to explore live global traffic trends.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://umesh-malik.com/blog/traffic-anomaly-or-outage-baseline-method" rel="noopener noreferrer"&gt;umesh-malik.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep reading on umesh-malik.com:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://umesh-malik.com/blog/remove-cloudflare-beacon-min-js" rel="noopener noreferrer"&gt;Remove Cloudflare beacon.min.js: you must opt in to opt out&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://umesh-malik.com/blog/zero-downtime-database-migration-dual-writes" rel="noopener noreferrer"&gt;Zero downtime database migration: 5 flags and a 17x P90 gap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://umesh-malik.com/blog/kubernetes-on-bare-metal-cloud-integrations" rel="noopener noreferrer"&gt;Kubernetes on bare metal: the 4 cloud integrations you must build&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>observability</category>
      <category>cloudflare</category>
      <category>networking</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
