<?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: 301ST</title>
    <description>The latest articles on DEV Community by 301ST (@301st).</description>
    <link>https://dev.to/301st</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4043581%2Ffe5435fc-2111-44a0-8981-964a6a57b876.png</url>
      <title>DEV Community: 301ST</title>
      <link>https://dev.to/301st</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/301st"/>
    <language>en</language>
    <item>
      <title>The .com price rises on 1 November. Lock today's for up to ten years</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Mon, 21 Sep 2026 14:00:18 +0000</pubDate>
      <link>https://dev.to/301st/the-com-price-rises-on-1-november-lock-todays-for-up-to-ten-years-2epn</link>
      <guid>https://dev.to/301st/the-com-price-rises-on-1-november-lock-todays-for-up-to-ten-years-2epn</guid>
      <description>&lt;p&gt;On 1 November every .com renewal gets more expensive. Verisign, which runs the .com registry, announced it with its quarterly results on 23 April:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Verisign announces that it will increase the annual registry-level wholesale fee for each new and renewal .com domain name registration from $10.26 to $10.97 effective Nov. 1, 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is 71 cents per domain per year, and it is the first of up to four such steps. The same registry contract that allows the steps also gives every domain owner a way around them. Section 7.3(f) of the .com Registry Agreement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Registry Operator shall provide no less than six months prior notice in advance of any increase for new and renewal domain name registrations and for transferring a domain name registration from one ICANN-accredited registrar to another and shall continue to offer for periods of up to ten years new and renewal domain name registrations fixed at the price in effect at the time such offer is accepted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Until 1 November, a renewal of up to ten years is charged at today's wholesale price, at any registrar. The rest of this article is about which of your domains to do that for, how many years each one can take, and what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the contract allows from here
&lt;/h2&gt;

&lt;p&gt;The price can move once per Pricing Year, which the agreement defines as 26 October to 25 October, and only in the last four Pricing Years of each six year period. The first period started on 26 October 2018, so the current one runs to October 2030 and its four increase years start in October 2026, 2027, 2028 and 2029. Each step is capped at 7 percent. In the previous period Verisign took all four, the last one bringing the price to $10.26 in September 2024.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;Wholesale&lt;/th&gt;
&lt;th&gt;At Cloudflare Registrar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;today&lt;/td&gt;
&lt;td&gt;$10.26&lt;/td&gt;
&lt;td&gt;$10.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 November 2026&lt;/td&gt;
&lt;td&gt;$10.97&lt;/td&gt;
&lt;td&gt;$11.17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26 October 2027&lt;/td&gt;
&lt;td&gt;up to about $11.7&lt;/td&gt;
&lt;td&gt;about $11.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26 October 2028&lt;/td&gt;
&lt;td&gt;up to about $12.6&lt;/td&gt;
&lt;td&gt;about $12.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26 October 2029&lt;/td&gt;
&lt;td&gt;up to about $13.4&lt;/td&gt;
&lt;td&gt;about $13.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rows after November are ceilings, not announcements; each one needs its own six months' notice. The Cloudflare column is the wholesale price plus ICANN's fee of $0.20 per domain year, which has applied since 1 July 2025. Cloudflare adds nothing on top, and its public domain search showed .com at $10.46 on 19 September 2026. If you see $10.44 quoted somewhere, it was computed with the old fee.&lt;/p&gt;

&lt;h2&gt;
  
  
  The date that sets your price is the renewal date
&lt;/h2&gt;

&lt;p&gt;A domain does not renew on its expiry date. Cloudflare's documentation, updated 24 April 2026:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The first auto-renew attempt will occur approximately 30 days prior to expiration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a .com expiring on 15 November renews in mid October, at $10.46. One expiring on 15 December renews in mid November, at $11.17. Auto-renew keeps a domain registered; it does not lock a price for the years ahead. A manual renewal for several years does. Auto-renew has &lt;a href="https://301.sh/auto-renew-on-domain-still-expired/" rel="noopener noreferrer"&gt;failure modes of its own&lt;/a&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many years each domain can take
&lt;/h2&gt;

&lt;p&gt;The registry caps a registration at ten years from today:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Renewal registrations of Registered Names (where available according to functional specifications and other requirements) may be made in the registry for terms not to exceed a total of ten years.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A domain that expires next summer can take nine more years. One already paid up to 2033 can take three. Verisign publishes the expiry date of every .com over RDAP, so the headroom for a whole list is one loop. The script needs &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt; and GNU &lt;code&gt;date&lt;/code&gt;; on macOS use &lt;code&gt;date -j -f '%Y-%m-%dT%H:%M:%SZ'&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-26s %-30s %-11s %s\n'&lt;/span&gt; DOMAIN REGISTRAR EXPIRES CAN_ADD

&lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; domain&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue

  &lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://rdap.verisign.com/com/v1/domain/&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.events[]? | select(.eventAction=="expiration") | .eventDate'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$expires&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-26s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"not registered"&lt;/span&gt;
    &lt;span class="k"&gt;continue
  fi
  &lt;/span&gt;&lt;span class="nv"&gt;registrar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.entities[]? | select(.roles | index("registrar"))
                     | .vcardArray[1][] | select(.[0]=="fn") | .[3]'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# The registry caps a registration at ten years from today.&lt;/span&gt;
  &lt;span class="nv"&gt;left&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$expires&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; now&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$left&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;can_add&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"expired"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;can_add&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3652&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; left&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;365&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-26s %-30.30s %-11s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$registrar&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;%%T*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$can_add&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;0.5
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; domains.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A run on 19 September 2026 against some well known names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOMAIN                     REGISTRAR                      EXPIRES     CAN_ADD
example.com                RESERVED-Internet Assigned Num 2027-08-13  9
cloudflare.com             Cloudflare, Inc.               2033-02-17  3
google.com                 MarkMonitor Inc.               2028-09-14  8
verisign.com               CSC Corporate Domains, Inc.    2034-06-01  2
namecheap.com              NameCheap, Inc.                2027-08-11  9
godaddy.com                GoDaddy.com, LLC               2032-11-01  3
stripe.com                 SafeNames Ltd.                 2027-09-11  9
github.com                 MarkMonitor Inc.               2028-10-09  7
this-domain-does-not-exist-9f3.com not registered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint serves .com only. The &lt;code&gt;REGISTRAR&lt;/code&gt; column matters as much as the count: it tells you where each renewal has to be clicked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic for one domain
&lt;/h2&gt;

&lt;p&gt;Take a .com that expires in August 2027, so it can take nine years.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Renewed now at Cloudflare, nine years cost 9 × $10.46 = $94.14, paid today.&lt;/li&gt;
&lt;li&gt;Renewed year by year, if Verisign stops after November: 9 × $11.17 = $100.53.&lt;/li&gt;
&lt;li&gt;Renewed year by year, if Verisign takes every step at the ceilings above and none after 2030: about $118.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The saving is roughly $6 to $23 per domain over nine years, and more if the pattern repeats after 2030, which the contract allows. Against that, the $94.14 leaves your account today, and it does not come back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All renewals are final and Cloudflare will not issue refunds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Which domains to lock
&lt;/h2&gt;

&lt;p&gt;A domain you will keep no matter what, the name of a business or a site with traffic, should take every year it can. The saving is certain and the money would have been spent anyway.&lt;/p&gt;

&lt;p&gt;A domain that only redirects is a judgement you can make with numbers. The &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;portfolio audit&lt;/a&gt; sorts domains by whether they still answer and how much authority they carry. The ones it marks as worth fixing are worth locking. The ones it marks to let lapse are the trap here: nine prepaid years on a domain you drop after two leave seven of them, $73.22, paid for nothing.&lt;/p&gt;

&lt;p&gt;A domain with two or three years of headroom left gains little either way, because most of its term is already paid at an older price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it at Cloudflare
&lt;/h2&gt;

&lt;p&gt;In the dashboard: &lt;strong&gt;Manage domains&lt;/strong&gt;, the domain's &lt;strong&gt;Manage&lt;/strong&gt;, then &lt;strong&gt;Renew/Extend Domain&lt;/strong&gt; under &lt;strong&gt;Registration&lt;/strong&gt;. The &lt;strong&gt;Renew for&lt;/strong&gt; menu goes up to 10 years. The added years land after the current expiry, not after today:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When renewing a domain, additional years are always added to the current expiration date regardless of when the renewal takes place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is no bulk path through the API. The Registrar API searches, checks, registers and lists domains, and its one call that changes an existing registration says so plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This endpoint currently supports updating &lt;code&gt;auto_renew&lt;/code&gt; only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a portfolio that means the dashboard, and the documented path renews one domain at a time. Start well before 31 October. The contract fixes the price in effect when the registry accepts the order, and a renewal sits in &lt;strong&gt;Renewal Pending&lt;/strong&gt; until it completes.&lt;/p&gt;

&lt;p&gt;At other registrars the same contract applies, but the price you pay is theirs, including their markup, and what they charge after 1 November is their decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  When 301.st helps, and where it stops
&lt;/h2&gt;

&lt;p&gt;For a handful of .com names in one Cloudflare account you do not need anything else: run the script, open the dashboard, and it is ten minutes of work.&lt;/p&gt;

&lt;p&gt;A portfolio spread across several registrars starts with a different question: which domains expire when, and where each one is registered. &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; fills in the expiry date of every domain it manages from RDAP, and from whois for .ru, whatever the registrar, and sorts the list by it. That list is the order to work through before 31 October. It does not renew anything, and through Cloudflare's API nothing can. The clicks at each registrar are still yours.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Certificates now expire in 200 days. Find the domains you still renew by hand</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Sun, 20 Sep 2026 14:00:18 +0000</pubDate>
      <link>https://dev.to/301st/certificates-now-expire-in-200-days-find-the-domains-you-still-renew-by-hand-29b2</link>
      <guid>https://dev.to/301st/certificates-now-expire-in-200-days-find-the-domains-you-still-renew-by-hand-29b2</guid>
      <description>&lt;p&gt;If you bought a one year certificate for a domain this spring, the certificate you installed lasts 199 days, and the reissue for the rest of the year is yours to install. The rules for publicly trusted certificates changed on 15 March 2026, and DigiCert and Sectigo moved before that date: DigiCert has issued nothing longer than 199 days since 24 February, Sectigo since 12 March. A DigiCert certificate issued on 24 February ran out at the end of 10 September. One issued on 15 March for the full 200 days runs out at the end of September.&lt;/p&gt;

&lt;p&gt;The 200 days are the first step of a schedule written into the Baseline Requirements, the rules every publicly trusted certificate authority follows. Version 2.3.0, dated 7 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Certificate issued&lt;/th&gt;
&lt;th&gt;Longest allowed&lt;/th&gt;
&lt;th&gt;Domain check reusable for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;before 15 March 2026&lt;/td&gt;
&lt;td&gt;398 days&lt;/td&gt;
&lt;td&gt;398 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;from 15 March 2026&lt;/td&gt;
&lt;td&gt;200 days&lt;/td&gt;
&lt;td&gt;200 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;from 15 March 2027&lt;/td&gt;
&lt;td&gt;100 days&lt;/td&gt;
&lt;td&gt;100 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;from 15 March 2029&lt;/td&gt;
&lt;td&gt;47 days&lt;/td&gt;
&lt;td&gt;10 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The authorities are not waiting for the dates. DigiCert's own table puts its next step, 99 days, in early 2027. Let's Encrypt runs further ahead: from 10 February 2027 its default certificates last 64 days, from 16 February 2028 they last 45, and the time a domain check stays valid drops to 10 days and then to 7 hours.&lt;/p&gt;

&lt;p&gt;The right column is the one that hurts a manual process. A certificate ordered by hand starts with proving control of the domain, by a DNS record, a file or an email, unless the last proof is recent enough. From March 2029 recent enough means ten days, so for a 47 day certificate the proof is part of every order. A domain whose certificate a person renews needs that person about twice a year now, about four times a year from 2027 and about eight times from 2029.&lt;/p&gt;

&lt;p&gt;On a domain that only redirects, nobody has to do this at all. The rest of this article finds the domains where somebody still does. If you are still choosing how to redirect in the first place, &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every way to redirect on Cloudflare&lt;/a&gt; compares the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  A redirect domain should have no certificate you renew
&lt;/h2&gt;

&lt;p&gt;A parked domain, or one that only forwards, needs a certificate for a single reason: a visitor who types &lt;code&gt;https://&lt;/code&gt; has to finish the TLS handshake before your &lt;code&gt;301&lt;/code&gt; can be sent. Nothing else on that domain uses it. Put the record behind Cloudflare's proxy and the certificate becomes Cloudflare's job. The documentation, updated 16 April 2026:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Universal certificates have a 90-day validity period. The auto renewal period starts 30 days before expiration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Universal SSL is included on every plan, Free too. Its 90 days are already inside the 2026 and 2027 caps. They are longer than the 47 days of 2029, and closing that gap is not your job either:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For Universal certificates, Cloudflare controls the validity periods and certificate authorities (CAs), making sure that renewal always occur.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The setup that removes the question entirely is the one from the &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;portfolio runbook&lt;/a&gt;: a proxied &lt;code&gt;A&lt;/code&gt; record pointing at &lt;code&gt;192.0.2.1&lt;/code&gt;, an address reserved for documentation that no server answers on, and a redirect rule at the edge. There is no server behind the domain, so there is no server certificate. The only certificate in the path is the one Cloudflare renews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sort your domains by who renews them
&lt;/h2&gt;

&lt;p&gt;You do not need an inventory to find the domains that do not fit that setup. The certificate each domain serves carries its own start and end dates, and the distance between them tells you a lot about how it got there. The script below needs only &lt;code&gt;openssl&lt;/code&gt; and GNU &lt;code&gt;date&lt;/code&gt;, the same pair the &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt; uses for its certificate column. On macOS use &lt;code&gt;date -j -f '%b %d %T %Y %Z'&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-34s %-24s %-9s %s\n'&lt;/span&gt; DOMAIN ISSUER LIFETIME LEFT

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; domain&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue

  &lt;/span&gt;&lt;span class="nv"&gt;cert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-connect&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;:443"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
         | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-startdate&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cert&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-34s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"no certificate"&lt;/span&gt;
    &lt;span class="k"&gt;continue
  fi

  &lt;/span&gt;&lt;span class="nv"&gt;issuer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cert&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^issuer=.*O *= *\([^,]*\).*/\1/p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cert&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^notBefore=//p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cert&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^notAfter=//p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;life&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; from &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;43200&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;left&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-34s %-24.24s %-9s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;issuer&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;life&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;d"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;left&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;d"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; domains.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lifetime is rounded to the nearest day, because authorities pad both ends by hours. A run on 19 September 2026 against this site, our Russian host, and a few large sites for contrast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOMAIN                             ISSUER                   LIFETIME  LEFT
301.sh                             Google Trust Services    90d       89d
www.301.sh                         Google Trust Services    90d       89d
ru.301.sh                          Let's Encrypt            90d       72d
www.paypal.com                     DigiCert Inc             199d      176d
www.salesforce.com                 DigiCert Inc             199d      13d
www.zoom.us                        DigiCert Inc             366d      101d
www.adobe.com                      DigiCert Inc             365d      107d
this-domain-does-not-exist-9f3.com no certificate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LIFETIME&lt;/code&gt; column is the one to sort by.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;90 days or less.&lt;/strong&gt; A machine issued this and a machine will replace it. Behind Cloudflare's proxy you should see 90 days from Google Trust Services, Let's Encrypt or SSL.com; &lt;code&gt;301.sh&lt;/code&gt; was reissued a few hours before the run. A short lifetime does not tell you whose machine it is. &lt;code&gt;ru.301.sh&lt;/code&gt; is not behind Cloudflare at all: it is served by GitHub Pages, and GitHub renews its Let's Encrypt certificate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;199 or 200 days.&lt;/strong&gt; The longest certificate a commercial authority issues under the new rules, the kind that comes with an order. The &lt;code&gt;www.salesforce.com&lt;/code&gt; certificate was issued on 18 March and has 13 days left. Whoever renews it will do it again within 200 days, and from 15 March 2027 within 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;365 days or more.&lt;/strong&gt; Issued before 15 March 2026, and among the last of their kind. The next certificate for that domain lasts 200 days at most, or 100 if it is ordered after 15 March 2027. On these domains the renewal date moves closer and nothing announces it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No certificate.&lt;/strong&gt; The handshake never happened: the name does not resolve, or nothing answers on port 443. Visitors typing &lt;code&gt;https://&lt;/code&gt; get an error before any redirect can run.&lt;/p&gt;

&lt;p&gt;On a portfolio where every domain is meant to sit behind Cloudflare, a lifetime over 90 days means Cloudflare is not the one serving that domain. On Business and Enterprise it can be a custom certificate you uploaded, which Cloudflare does not renew either. Everywhere else it is a record that was never proxied, or a domain whose nameservers never moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The certificate the script does not see
&lt;/h2&gt;

&lt;p&gt;Behind the proxy, the script reads Cloudflare's certificate, the one visitors get. When Cloudflare forwards the request to your server, there is a second certificate on that connection. In SSL/TLS mode Full (Strict), Cloudflare checks it, and on the day it expires visitors get a &lt;a href="https://301.sh/redirect-silently-fails/" rel="noopener noreferrer"&gt;&lt;code&gt;526&lt;/code&gt;&lt;/a&gt; instead of your page or your redirect. No browser ever sees this certificate, so nothing warns you before that day.&lt;/p&gt;

&lt;p&gt;To read it, connect to the server's address directly and name the domain:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;There are three ways to take this certificate off your calendar.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automate it on the server.&lt;/strong&gt; Any ACME client renews on its own. For the move to 64 days in February 2027, the advice from Let's Encrypt is one sentence: "you should verify that your automation is compatible with certificates that have shorter validity periods."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a Cloudflare Origin CA certificate.&lt;/strong&gt; It is free on every plan and trusted by Cloudflare and nobody else, so the public schedule does not apply to it: the API offers validity from 7 days up to 5,475, which is 15 years. Cloudflare's own page names two conditions. The first:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloudflare does not currently send expiration notifications for origin CA certificates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second: it only works while the record stays proxied.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Site visitors may see untrusted certificate errors if you pause Cloudflare or disable proxying on subdomains that use Cloudflare origin CA certificates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Write the expiry date down somewhere you will look, because Cloudflare will not remind you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Take the server out of the path.&lt;/strong&gt; If the server exists only to send a redirect, move the redirect to Cloudflare and point the record at &lt;code&gt;192.0.2.1&lt;/code&gt; as above. The second certificate goes away with the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where this breaks
&lt;/h2&gt;

&lt;p&gt;Universal SSL has conditions. On a portfolio, each of them is a domain somewhere in the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The record is not proxied.&lt;/strong&gt; A record set to DNS only gets no certificate from Cloudflare:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloudflare can only serve an SSL/TLS certificate for a DNS record when you set the record's proxy status to Proxied. If you do not do this, the origin server your record points to will be responsible for supporting SSL/TLS connections.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The zone is still pending.&lt;/strong&gt; Universal SSL is issued to domains added to and activated on Cloudflare. Until the nameservers change at the registrar there is no certificate at all, and adding many zones at once runs into &lt;a href="https://301.sh/api-token-cannot-add-more-zones/" rel="noopener noreferrer"&gt;a limit of its own&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The name is two levels deep.&lt;/strong&gt; On a full setup the certificate covers &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt;, but not &lt;code&gt;www.shop.example.com&lt;/code&gt;. The fix is Advanced Certificate Manager with Total TLS, listed at $10 a month on Cloudflare's plans page on 19 September 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare will not certify the name.&lt;/strong&gt; From the limitations page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Some domains are not eligible for Universal SSL if they contain words that conflict with trademarked domains.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A portfolio of names close to brands will find some of its domains here. Cloudflare's way out is a paid advanced certificate, or a custom one on Business and Enterprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A CAA record elsewhere in the chain.&lt;/strong&gt; When you add CAA records to a zone with Universal SSL, Cloudflare adds the ones its authorities need. It cannot change the records of a zone you point to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your hostname CNAMEs to a domain whose zone has restrictive CAA records, those records take precedence — even if your own domain has no CAA records.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When 301.st is worth it
&lt;/h2&gt;

&lt;p&gt;For a few domains you do not need us. Proxy the records, move the redirects to the edge, and run the script once a quarter. It takes a minute.&lt;/p&gt;

&lt;p&gt;With hundreds of domains the setup has to happen at the moment a domain is added. &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; builds it for every domain it adds: the apex gets a proxied &lt;code&gt;A&lt;/code&gt; record at &lt;code&gt;192.0.2.1&lt;/code&gt; and the redirect runs at the edge, so those domains have no server certificate at all. It also reads the certificate state of every zone from Cloudflare each day and shows which domains have a certificate that is pending or failing, such as one stuck on validation because of a CAA record two hops away.&lt;/p&gt;

&lt;p&gt;It does not look at certificates installed on your own servers, and a record someone later switches to DNS only is not a certificate error in Cloudflare's eyes. The record shows up in the script's &lt;code&gt;LIFETIME&lt;/code&gt; column, the server certificate in the &lt;code&gt;openssl&lt;/code&gt; line from the previous section. Put both in a cron job.&lt;/p&gt;

</description>
      <category>tls</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The zone limit your API token hits and your Global API Key does not</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:00:39 +0000</pubDate>
      <link>https://dev.to/301st/the-zone-limit-your-api-token-hits-and-your-global-api-key-does-not-293i</link>
      <guid>https://dev.to/301st/the-zone-limit-your-api-token-hits-and-your-global-api-key-does-not-293i</guid>
      <description>&lt;p&gt;Cloudflare's own guide for adding many sites by script ends with two limits. The page was last updated on 5 May 2026 and it says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are limitations on the number of domains you can add at a time - specifically, you can only sign up a maximum of 25 domains every 10 minutes.&lt;/p&gt;

&lt;p&gt;In addition, if you have over 50 domains and, of those domains, more are pending than active, you will be blocked from adding more. We recommend waiting until your pending sites have been activated before adding more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;portfolio runbook&lt;/a&gt; on this site reads the second one the way it is written: a ratio with a threshold. Under fifty domains there is nothing to plan around. That is also what we believed when 301.st started adding zones for the accounts it onboards, with a scoped API token, the credential Cloudflare tells you to use. This is what came back:&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You have exceeded the limit for adding zones. Please activate some zones."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&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;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;It came back on accounts with a handful of domains. On a fresh account it came back on the second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that fits the evidence
&lt;/h2&gt;

&lt;p&gt;The error is old and the community has been reporting it since before the guide had a number in it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;March 2019.&lt;/strong&gt; A Cloudflare staff member closes a thread about code 1118: there is a limit on the number of pending zones at any one time, and validating pending zones is what raises it. No threshold is named.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 2021.&lt;/strong&gt; A poster quotes the support article of the day, which had no threshold either: you cannot have more pending sites than active sites on the account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;December 2024.&lt;/strong&gt; Ten zones, three active, seven pending, blocked. The poster had read that the limit was fifty and asks why theirs is ten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;January 2025.&lt;/strong&gt; A Free account with one zone. Adding the second returns 1118. The poster deletes everything and tries again: with no zones, one goes in; the next does not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule fits every case, and it is the rule we see on the accounts we onboard: a new zone is refused once the account has more pending zones than active ones. The fifty domain threshold from the guide did not show up in any of them. Put plainly, you can add about as many zones as you already have active, and a new account with nothing active gets one.&lt;/p&gt;

&lt;p&gt;That alone changes the plan for a portfolio. It is not add fifty, activate, add fifty. It is add one, activate it, add two, activate them, add four. The ceiling doubles with each round and the first rounds are the slow ones, because activation waits on a nameserver change at the registrar and on Cloudflare noticing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that depends on who is asking
&lt;/h2&gt;

&lt;p&gt;Here is the piece we have not found written down anywhere.&lt;/p&gt;

&lt;p&gt;The same account, the same list of domains, the same request to the same endpoint. With an API token, scoped as Cloudflare recommends, the block lands as soon as pending outnumbers active. With the user's Global API Key, the batch goes in. The only limits we have run into with the key are the two documented ones: 25 domains per 10 minutes, and the ratio once the account is past fifty.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request signed with&lt;/th&gt;
&lt;th&gt;A new zone is refused when&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API token, user or account owned&lt;/td&gt;
&lt;td&gt;pending zones outnumber active ones, from the second domain on a fresh account&lt;/td&gt;
&lt;td&gt;observed on the accounts we onboard, matches every thread above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global API Key&lt;/td&gt;
&lt;td&gt;over 50 domains on the account and pending outnumber active&lt;/td&gt;
&lt;td&gt;the guide, and what we see with the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;more than 25 domains in 10 minutes&lt;/td&gt;
&lt;td&gt;the guide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our reading is that the key acts as the user, the same principal as the dashboard, while a token is automation, and the pending cap applied to automation is the stricter one. That is a reading, not a statement from Cloudflare. Treat the whole section as observed behaviour on accounts we run, and check it on yours before you build on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your account in two requests
&lt;/h2&gt;

&lt;p&gt;Count what you have. The zones endpoint filters on status and reports the total without paging through the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;s &lt;span class="k"&gt;in &lt;/span&gt;active pending&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s: '&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones?account.id=&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;status=&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;per_page=1"&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; | jq &lt;span class="s1"&gt;'.result_info.total_count'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If pending is already at or above active, try adding one domain you own with the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones"&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;--data&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;account&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;},&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;full&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.success, .errors'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;false&lt;/code&gt; followed by code 1118 means your account is under the rule above, not under the one in the guide. We have not found a plan that lifts it; the staff answer from 2019 names activation as the only lever. From here there are three ways forward.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Activate what is pending.&lt;/strong&gt; Change the nameservers at the registrar, wait for the zone to go active, and the ceiling rises by one. This is the right answer and the slow one: hours per round, sometimes a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add through the dashboard by hand.&lt;/strong&gt; One at a time, which is the work you were scripting away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Global API Key for the adding step.&lt;/strong&gt; Which raises the question of where that key is allowed to live.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why the key must not leave your browser
&lt;/h2&gt;

&lt;p&gt;Cloudflare's page on the Global API Key lists what is wrong with it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Access to all Cloudflare resources&lt;/strong&gt; - Global API key has access to all of a user's resources. This makes it impossible to safely use Global API key to access non-production resources when a user also has access to production resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full permissions&lt;/strong&gt; - Similarly, Global API key has the exact same permissions as the user, which means if the user can delete zones or change DNS records, so can the Global API key.&lt;/p&gt;

&lt;p&gt;For these reasons, Global API key is not recommended for new customers. Current customers using Global API key are encouraged to migrate and use API tokens instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A platform that stores your key holds everything you hold. 301.st does not store it: onboarding takes a scoped token, and the quick path that accepts a key uses it once to mint a scoped token and discards it. That is the right design, and it means the server side of 301.st cannot perform the one step where the key is what gets through.&lt;/p&gt;

&lt;p&gt;So the adding step moved to the one place where the key can stay yours: your browser. &lt;a href="https://chromewebstore.google.com/detail/gncbekdjakchefiiahjbjlbhhfijoikp" rel="noopener noreferrer"&gt;Cloudflare Tools&lt;/a&gt; is an extension we wrote in January 2026 for exactly this, and it is &lt;a href="https://github.com/investblog/cloudflare-tools" rel="noopener noreferrer"&gt;open source&lt;/a&gt;. You paste the key into the side panel. It is encrypted with AES-256-GCM under a random key that lives in session storage and is gone when the browser closes. Every request goes from your browser to api.cloudflare.com and nowhere else; no server of ours is in the path. Paste a domain list, run a preflight that sorts it into will create, exists, invalid and duplicate, and start. The batch runs at Cloudflare's pace, 25 per 10 minutes, backs off on a 429 with &lt;code&gt;Retry-After&lt;/code&gt;, and resumes after a browser restart. The same panel does check and export to CSV, bulk delete and bulk cache purge.&lt;/p&gt;

&lt;p&gt;It is our tool, not Cloudflare's, and it is not affiliated with or endorsed by Cloudflare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in 0.2.0
&lt;/h2&gt;

&lt;p&gt;Version 0.2.0 shipped on 1 September 2026. It is live on Firefox Add-ons and going through review on the Chrome Web Store and Edge Add-ons, so those two links carry the previous version until it clears.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens, not only the key.&lt;/strong&gt; User tokens (&lt;code&gt;cfut_&lt;/code&gt;) and account owned tokens (&lt;code&gt;cfat_&lt;/code&gt;) are accepted alongside the Global API Key, and the kind is detected from the pasted secret. When your account already has active zones to spend, a token with Zone, Zone, Edit on the account is enough and the key never has to be pasted anywhere. Add Cache Purge if you purge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profiles.&lt;/strong&gt; Several accounts or credentials, switched from the header. A running batch stays on the profile it started under, whatever you switch to meanwhile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The toolbar button opens the panel directly&lt;/strong&gt;, the UI follows the browser language in English and Russian, and the publisher news feed is opt in and off by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next release, 0.3.0, closes the loop on the limit itself: when one account's pending count hits its ceiling, the rest of the list spills to the next profile. The limit is per account, so a portfolio spread over several accounts is the ceiling multiplied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the extension stops
&lt;/h2&gt;

&lt;p&gt;It adds zones. It does not move them from pending to active, because that happens at the registrar, and it does not tell you next month which of them are still pending, which record went grey, or which domain was supposed to point where. Its job ends when the batch is in.&lt;/p&gt;

&lt;p&gt;Keeping the portfolio true after that is what &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; does, and the &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;runbook&lt;/a&gt; lists the steps it takes over. For the batch in front of you today, install the extension and paste the list.&lt;/p&gt;

</description>
      <category>limits</category>
      <category>dns</category>
      <category>browser</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Your redirect rule says Active and nothing happens</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Tue, 18 Aug 2026 14:00:57 +0000</pubDate>
      <link>https://dev.to/301st/your-redirect-rule-says-active-and-nothing-happens-46ec</link>
      <guid>https://dev.to/301st/your-redirect-rule-says-active-and-nothing-happens-46ec</guid>
      <description>&lt;p&gt;The rule is in the list. Its toggle says Active. You open the URL and the old page loads, exactly as before. Nothing in the dashboard suggests a problem, because nothing in the dashboard is wrong: Active means the rule was saved and is eligible to run. It says nothing about whether a request ever reached it.&lt;/p&gt;

&lt;p&gt;That gap is the whole diagnosis. Four questions close it, in this order, and each one is answered by a single request rather than by another look at the configuration. We ran every check in this article against a live free-plan zone on 17 August 2026, including the ones whose answer is "the rule does not fire" — those are the useful ones. If you are still choosing a method, the &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;comparison of every way to redirect on Cloudflare&lt;/a&gt; is the map; this is what to do when the method you picked appears to do nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question one: does the request reach your zone at all
&lt;/h2&gt;

&lt;p&gt;Every other explanation assumes Cloudflare saw the request. Check that first, and check it with a rule rather than with a header, because a rule is the thing whose absence you are diagnosing.&lt;/p&gt;

&lt;p&gt;Add a temporary redirect rule on a path that nothing on your site serves, matching exactly, and point it anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(http.request.uri.path eq "/zone-probe-9f3")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="s2"&gt;"https://your-domain/zone-probe-9f3"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;302&lt;/code&gt; means your zone terminated that request and your rules run. A &lt;code&gt;404&lt;/code&gt; from your own application, or anything else your origin would say, means they do not — and no amount of editing the redirect rule will change that. On our zone the probe went from &lt;code&gt;404&lt;/code&gt; before the rule to &lt;code&gt;302&lt;/code&gt; about three seconds after it was created, and back to &lt;code&gt;404&lt;/code&gt; after it was deleted, though the deletion took longer to reach every edge server than the creation did. A first response is not evidence in either direction; ask twice.&lt;/p&gt;

&lt;p&gt;If the probe stays negative, there are three ways to be outside your own zone, and they look identical from the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The record is not proxied.&lt;/strong&gt; Cloudflare states the requirement plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Single Redirects and Bulk Redirects require that you proxy the DNS records of your domain (or subdomain) through Cloudflare.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A grey cloud means DNS only: visitors get your origin's address and connect to it directly. There is a warning for this, but it is narrow. The dashboard raises &lt;em&gt;This rule may not apply to your traffic&lt;/em&gt; only when your expression names a hostname that has no proxied record. A rule written as &lt;code&gt;http.request.uri.path eq "/"&lt;/code&gt; names no hostname, so it saves in silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The zone never activated.&lt;/strong&gt; If the nameservers at the registrar were never switched, the zone sits pending and everything you configured is real but not live. That failure has its own signature and its own &lt;a href="https://301.sh/redirect-silently-fails/" rel="noopener noreferrer"&gt;article on silent redirect failures&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your host is on Cloudflare too.&lt;/strong&gt; This is the one that fools people, because everything on your side looks right. If your hostname is onboarded to a platform that uses Cloudflare for SaaS, the arrangement Cloudflare calls Orange-to-Orange decides whose rules run, and it has exact conditions: the record must be a &lt;strong&gt;CNAME&lt;/strong&gt; to the provider's target, it must be &lt;strong&gt;proxied&lt;/strong&gt; in your zone, and the two zones must be in &lt;strong&gt;different accounts&lt;/strong&gt;. When that holds, your zone is first in line and your rules do run. When it does not hold — an &lt;code&gt;A&lt;/code&gt; record to the provider's address, or no zone of your own — the provider's zone is the only one applying settings, and your redirect rule is a note in a file nobody reads. There is no flag to check: Cloudflare's documentation says outright that no API field or zone setting reports whether Orange-to-Orange is active.&lt;/p&gt;

&lt;p&gt;Two traps around this check. &lt;code&gt;https://your-domain/cdn-cgi/trace&lt;/code&gt; proves you are behind &lt;em&gt;a&lt;/em&gt; Cloudflare, not behind &lt;em&gt;yours&lt;/em&gt;, so it cannot settle the question — and we confirmed the flip side, that a redirect rule matching &lt;code&gt;/cdn-cgi/trace&lt;/code&gt; does not fire at all, leaving that endpoint honest. And zone analytics showing traffic proves the hostname resolves somewhere in your account's view, not that this request was evaluated by your ruleset.&lt;/p&gt;

&lt;p&gt;The dashboard's own confirmation is &lt;a href="https://developers.cloudflare.com/rules/trace-request/" rel="noopener noreferrer"&gt;Cloudflare Trace&lt;/a&gt;, in beta and on every plan. It simulates a request and lists the configurations that matched, in evaluation order. Its input has the hint built in: the URL "must include a hostname that belongs to your account". If Trace will not accept the hostname, the answer to question one is no. Note that Trace runs against your configuration, not against live traffic, so it cannot see the routing problem itself — only the probe request can. The API route for it exists, but it is scoped separately; a token that can create and delete redirect rules got &lt;code&gt;Authentication error&lt;/code&gt; from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question two: does the rule match what you are actually sending
&lt;/h2&gt;

&lt;p&gt;This is where most of the time goes, and where one behaviour accounts for more of it than everything else combined.&lt;/p&gt;

&lt;p&gt;A wildcard pattern is compared against the entire URL, query string included. Cloudflare's operator documentation says so in an example: a request to &lt;code&gt;https://sub.example.com/folder2/page.html?s=value&lt;/code&gt; does not match &lt;code&gt;*.example.com/*/page.html&lt;/code&gt; because "http.request.full_uri includes the query string and its full value does not match". Stated as a rule you can act on: &lt;strong&gt;a wildcard pattern that does not end in &lt;code&gt;*&lt;/code&gt; will never match a request that carries a query string.&lt;/strong&gt; Which is exactly what happens when you test the rule by adding &lt;code&gt;?v=2&lt;/code&gt; to defeat your browser cache. The cache-buster defeats the rule instead, the page loads normally, and you conclude the rule is broken.&lt;/p&gt;

&lt;p&gt;Here is that pair, measured, along with the rest of the matching behaviour worth knowing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;wildcard &lt;code&gt;https://301.sh/rule-probe/*.html&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rule-probe/x.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;302&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same rule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rule-probe/x.html?v=2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;404&lt;/code&gt;, no redirect&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same rule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rule-probe/deep/x.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;302&lt;/code&gt; — &lt;code&gt;*&lt;/code&gt; crosses slashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same rule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rule-probe/x.HTML&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;302&lt;/code&gt; — &lt;code&gt;wildcard&lt;/code&gt; ignores case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http.request.uri.path eq "/Rule-Probe-Case"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Rule-Probe-Case&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;302&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same rule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rule-probe-case&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;404&lt;/code&gt;, &lt;code&gt;eq&lt;/code&gt; is case-sensitive&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http.request.uri.path eq "/rule-probe-slash"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rule-probe-slash/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;404&lt;/code&gt;, the slash is part of the path&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The case row is worth a second look, because the two interfaces disagree. The &lt;code&gt;wildcard&lt;/code&gt; operator is case-insensitive by documentation and by measurement; &lt;code&gt;strict wildcard&lt;/code&gt; is the case-sensitive variant. But &lt;code&gt;eq&lt;/code&gt; on a path compares strings, so a rule typed with capitals from a design document quietly matches nothing that visitors send.&lt;/p&gt;

&lt;p&gt;Bulk Redirects have a different matching algorithm, and the difference cuts the other way. A bulk redirect matches on scheme, hostname and path only — &lt;strong&gt;the query string takes no part in matching at all&lt;/strong&gt;, so the cache-buster that kills a wildcard rule is irrelevant here. What substitutes for it is defaults: &lt;code&gt;Subpath matching&lt;/code&gt; and &lt;code&gt;Include subdomains&lt;/code&gt; are both off unless you turned them on, so &lt;code&gt;/blog&lt;/code&gt; does not cover &lt;code&gt;/blog/post&lt;/code&gt;, and &lt;code&gt;example.com&lt;/code&gt; does not cover &lt;code&gt;www.example.com&lt;/code&gt;. And a list on its own does nothing. Cloudflare checks the redirects of each list "that is enabled by a Bulk Redirect Rule" — uploading a list and never creating the rule that switches it on is a complete, silent no-op.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question three: did something answer before your rule got its turn
&lt;/h2&gt;

&lt;p&gt;Rules products run in a fixed order, and a redirect is a terminating action. Cloudflare puts it without hedging:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;for terminating actions (Block, Redirect, or one of the challenge actions), rule evaluation will stop and the action will be executed immediately&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three consequences, one of which we measured. Two redirect rules matching the same request: the first one wins, always — we deployed a pair pointing at different targets, and the answer carried the target of the first. Single Redirects run before Bulk Redirects in the product order, so when both have a rule for a URL, the Single Redirect is the one that answers. And Page Rules, if you still have them, lose to all of it: the modern rules products "take precedence over Page Rules". An old forwarding Page Rule is not what is fighting your new rule. It is what runs when your new rule does not match — which produces the more confusing symptom of a redirect happening to somewhere you did not configure.&lt;/p&gt;

&lt;p&gt;The same phase order is why a redirect cannot be counted at the edge that issues it, which is &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;its own article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question four: did it fire while you looked at a cached answer
&lt;/h2&gt;

&lt;p&gt;A redirect that works is easy to hide. A &lt;code&gt;301&lt;/code&gt; is a permanent redirect, and Cloudflare's rules send it without a &lt;code&gt;Cache-Control&lt;/code&gt; header, so a browser may cache it heuristically and keep sending you to the old target long after the rule changed. The reverse also holds: a browser that once got a &lt;code&gt;200&lt;/code&gt; for that URL can go on serving it from cache while the edge is redirecting everyone else. Test with &lt;code&gt;curl -sI&lt;/code&gt;, which caches nothing, before you test with a browser tab.&lt;/p&gt;

&lt;p&gt;And when you do reach for a cache-buster, remember question two: appending &lt;code&gt;?v=2&lt;/code&gt; changes whether a wildcard rule matches. The standard advice for making a browser fetch fresh is the standard way to make this particular test lie. Use a private window, a different browser, or &lt;code&gt;curl&lt;/code&gt; — not a query parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the redirect was never Cloudflare's to run
&lt;/h2&gt;

&lt;p&gt;One case belongs outside the tree. If the redirect lives at your origin — a rewrite in &lt;code&gt;.htaccess&lt;/code&gt;, a framework route — then Cloudflare has no rule to fire and nothing to trace. It forwards whatever the origin says, and the origin can be inconsistent in ways that are invisible from the dashboard. A report in July described exactly that: the same URL answering &lt;code&gt;301&lt;/code&gt; when proxied through one Cloudflare datacenter and &lt;code&gt;200&lt;/code&gt; when proxied through another, with the origin returning a clean &lt;code&gt;301&lt;/code&gt; on a direct request every time. Whatever the cause, the fix is structural. A redirect expressed as an edge rule answers before the origin is contacted, so it cannot vary with what the origin decides to do that second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this gets untidy
&lt;/h2&gt;

&lt;p&gt;A rule whose target equals its source produces a loop, and Cloudflare does not stop it. We pointed a rule at its own path and followed the chain: six hops in, still redirecting to itself. A browser calls that too many redirects; a monitoring check may call it a &lt;code&gt;302&lt;/code&gt; and pass.&lt;/p&gt;

&lt;p&gt;Free zones get 10 Single Redirect rules, and the quota is per zone. Hitting it fails at creation rather than at runtime, so it is not a cause of this symptom — but it is a reason people put logic into one wildcard rule, where the query-string behaviour above starts to bite. The full quota table sits in the &lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;article on redirect limits&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Trace does not evaluate inactive rules, which is correct and occasionally misleading: a rule switched off looks the same in Trace results as a rule that did not match. And Trace reflects configuration, so a request that never reaches your zone traces perfectly while failing in production. That is why question one comes first and is answered with a real request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs across a portfolio
&lt;/h2&gt;

&lt;p&gt;For one domain, this is a ten-minute job. Two &lt;code&gt;curl&lt;/code&gt; calls and a temporary rule tell you which of the four questions is the wrong one, and the fix follows from the answer.&lt;/p&gt;

&lt;p&gt;The arithmetic changes when the same question has to be asked of two hundred domains that nobody opened this month, because none of these failures announce themselves. A rule that stopped matching after a platform migration looks exactly like a rule that works, from the inside. That is the case for asking every domain the same question on a schedule — the loop from the &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt; if you like scripts, and &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; if you would rather the portfolio be watched continuously and tell you when a row changes. For a handful of domains, the script and a cron entry are genuinely enough.&lt;/p&gt;

</description>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>dns</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to audit agent readiness when the scanner cannot reach the site</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:00:58 +0000</pubDate>
      <link>https://dev.to/301st/how-to-audit-agent-readiness-when-the-scanner-cannot-reach-the-site-gg4</link>
      <guid>https://dev.to/301st/how-to-audit-agent-readiness-when-the-scanner-cannot-reach-the-site-gg4</guid>
      <description>&lt;p&gt;Cloudflare runs a scanner that grades a domain on how ready it is for AI agents. We went through &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;all twenty one of its checks&lt;/a&gt; in July: which four were worth implementing, and why eight of the failures were the right answer.&lt;/p&gt;

&lt;p&gt;You point it at your site, get a number, and fix what it flags. Then you try to check the version that actually matters — the staging build with the change in it, the site that has not launched, the pages behind a login — and there is nothing to point the scanner at.&lt;/p&gt;

&lt;p&gt;That is not a bug in the scanner. It runs in somebody else's datacenter, so it can only answer for origins that datacenter can reach. Both failure modes are one &lt;code&gt;curl&lt;/code&gt; away, checked 4 August 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://isitagentready.com/api/scan &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -H 'content-type: application/json' -d '{"url":"http://localhost:3000"}'
{"error":"Invalid URL provided"}

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://isitagentready.com/api/scan &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -H 'content-type: application/json' -d '{"url":"https://staging.301.sh"}'
{"url":"https://staging.301.sh","scannedAt":"2026-08-04T18:31:38.462Z",
 "siteError":{"httpStatus":530,"statusText":"","bodyPreview":null,
 "retryAfter":null,"server":"cloudflare"}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local address is rejected outright. A host the scanner cannot resolve returns a site error instead of a scorecard. Not a low score: no score.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds, because of when you want the answer. Taking this site from Level 1 to Level 5 meant a batch of static files, then a &lt;code&gt;/mcp&lt;/code&gt; server, then DNS records, and every one of those steps was a deploy to production first and a scan second. Publish, then find out. For a redirect you would never accept that loop.&lt;/p&gt;

&lt;p&gt;The other place to run the checks is the browser, which reaches whatever you can reach. This article is about what that changes, what it costs, and the one rule that decides whether the result is worth anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the browser has that the datacenter does not
&lt;/h2&gt;

&lt;p&gt;Three things, and only the first one is obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reachability.&lt;/strong&gt; &lt;code&gt;localhost:5173&lt;/code&gt;, a staging host behind a VPN, an internal tool, a domain whose DNS is not public yet. If the tab loads, the audit runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A session.&lt;/strong&gt; Your cookies are attached to the page request, so a site that shows an anonymous visitor a login wall gets graded on what a logged-in client actually receives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An in-page runtime.&lt;/strong&gt; Some of what an agent consumes is not an HTTP response at all. WebMCP exposes tools through &lt;code&gt;document.modelContext&lt;/code&gt; inside the page, after scripts run. An HTTP probe from outside cannot see it; a script in the page can.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that keeps a browser audit honest
&lt;/h2&gt;

&lt;p&gt;The session is also the trap, and it is worth understanding before you write your own script, never mind which tool you use.&lt;/p&gt;

&lt;p&gt;An agent readiness audit fetches two very different classes of thing. There is the page, which may legitimately live behind a login. And there are the machine files — &lt;code&gt;robots.txt&lt;/code&gt;, the sitemap, &lt;code&gt;llms.txt&lt;/code&gt;, everything under &lt;code&gt;/.well-known/&lt;/code&gt; — which are public by definition, because the client reading them has no account and never will.&lt;/p&gt;

&lt;p&gt;Send your session to both and you grade a site that nobody but you can see. A &lt;code&gt;/.well-known/&lt;/code&gt; document that only answers for authenticated requests is a document an agent gets a 401 or a login page from. Passing it in your browser tells you nothing true.&lt;/p&gt;

&lt;p&gt;So the split is per request, not per scan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What is fetched&lt;/th&gt;
&lt;th&gt;Credentials&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The page itself&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;It may be behind a login, and that is the case the external scanner cannot cover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Accept: text/markdown&lt;/code&gt; on the page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same URL, same session, a different representation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;sitemap.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public machine files — judge them as a crawler receives them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;llms.txt&lt;/code&gt;, &lt;code&gt;llms-full.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything under &lt;code&gt;/.well-known/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;omit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Discovery documents are read by clients with no account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Redirects follow the same logic. The probe follows hops the way a navigation does, each hop attaching its own cookies, and the response records the final URL. A check that passed on a URL you did not request is a check you want to see the address for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes shape when the audit moves into a browser
&lt;/h2&gt;

&lt;p&gt;Moving the audit into the browser is not a free upgrade. It trades one set of blind spots for another, and a tool that hides the trade is worse than the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS-AID needs a different question, not a different tool.&lt;/strong&gt; The extension APIs have no DNS resolver, which is where this write-up originally stopped — wrongly, because a resolver is not what you need. DNS-over-HTTPS is an ordinary &lt;code&gt;fetch&lt;/code&gt;, and its JSON answer carries both halves of the verdict: the SVCB records under &lt;code&gt;_index._agents&lt;/code&gt;, and &lt;code&gt;AD&lt;/code&gt;, the resolver's DNSSEC judgement. That second half matters, because a site publishing records without a validated chain is failed by the scanner too. No token and no account are involved, so the answer is the same for every user. On this site it was &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;the last check to pass&lt;/a&gt; — the resolver cache in front of it took about forty minutes of rescans after the records were already live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebMCP only works here.&lt;/strong&gt; In-page detection returns one of three answers, and the third one is the point: detected, confirmed absent, or detection unavailable. A browser without the API, or a page where the injected script could not run, produces "unavailable" — not a failure. Not detectable is not the same as not present, and a check that scores those the same way manufactures a defect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MCP Server Card needs five addresses.&lt;/strong&gt; The audit &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;already flagged this&lt;/a&gt;, and it has not improved: the draft-canonical path, the IETF one, the two Cloudflare probes and the de-facto legacy file are all in play because the specification moved and the scanners did not move with it. A checker that probes one of them reports the state of that address, not the state of your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks: the number is not one number
&lt;/h2&gt;

&lt;p&gt;Three separate reasons a score you read today will not match a score somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not-applicable checks leave the denominator.&lt;/strong&gt; The composite is passing checks over applicable ones, and a check that cannot apply is excluded rather than failed. Anything the browser genuinely cannot reach — a resolver that will not answer, an in-page runtime that never ran — drops out there and stays in from outside. Same site, same day, different arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare's own two surfaces disagree.&lt;/strong&gt; On 30 July 2026 this site read 79 of 100 and Level 5 "Agent-Native" in the web interface, while &lt;code&gt;/api/scan&lt;/code&gt; reported Level 4 for the same domain in the same hour. That is stable, not a glitch we caught mid-deploy, and it means "what level are we" has two correct answers depending on which endpoint you ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check matrix itself moves.&lt;/strong&gt; The MCP Server Card above is one example; DNS-AID is another, resting on an individual Internet-Draft that expires on 28 November 2026, probed through labels that draft does not define. And &lt;code&gt;llms.txt&lt;/code&gt; is not a scored check in either tool: the audit measures content negotiation, so a site can publish a complete &lt;code&gt;llms.txt&lt;/code&gt; surface and score zero on content accessibility, which is exactly how the July audit opened. What counts as readiness is being decided while you measure it.&lt;/p&gt;

&lt;p&gt;The conclusion is not that the score is worthless. It is that the score is only meaningful against itself. What did this origin do yesterday, and what changed after the last deploy. Comparing your number to another site's number compares two arithmetics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extension
&lt;/h2&gt;

&lt;p&gt;That is what &lt;a href="https://chromewebstore.google.com/detail/agent-readiness-inspector/diofmjhnegmcccocikabageabmaokobd" rel="noopener noreferrer"&gt;Agent Readiness Inspector&lt;/a&gt; does, and shipping it is the occasion for this article. It is our own extension, it runs 22 checks in the browser against whatever origin the current tab is on, and it is free. Firefox and Edge builds are with the stores for review; today the link above is the one that works.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22 checks, versioned as data.&lt;/strong&gt; Robots and AI crawler rules, sitemaps, &lt;code&gt;Link&lt;/code&gt; headers, Markdown negotiation, Content Signals, Agent Skills, API Catalog, MCP Server Card, OAuth discovery, Web Bot Auth, WebMCP, plus the agentic commerce protocols as an unscored preview. The matrix carries a version and a CI job watches the upstream one for drift, because the section above is a permanent condition rather than a bad month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence per check.&lt;/strong&gt; The response that produced the verdict, including the final URL after redirects, and a copy-ready prompt for the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved sites and regressions.&lt;/strong&gt; Scheduled rescans, history, and an alert when a check that used to pass stops passing. Alerts land in a local inbox first; the notification permission is optional and asked for only if you turn it on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first.&lt;/strong&gt; Scans, history, settings and alerts live in browser storage. The optional outside comparison uses your own Cloudflare URL Scanner credentials and is off by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is an independent implementation of open web standards, not affiliated with or endorsed by Cloudflare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tool to use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;isitagentready.com&lt;/th&gt;
&lt;th&gt;Agent Readiness Inspector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A public production site&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging, localhost, pre-launch, VPN&lt;/td&gt;
&lt;td&gt;no, site error or invalid URL&lt;/td&gt;
&lt;td&gt;yes, if the tab loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages behind a login&lt;/td&gt;
&lt;td&gt;anonymous view only&lt;/td&gt;
&lt;td&gt;your session, on the page probes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebMCP in the page&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, in-page detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS-AID&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes, over DNS-over-HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The score Cloudflare will quote at you&lt;/td&gt;
&lt;td&gt;yes, this is the source&lt;/td&gt;
&lt;td&gt;no, a second opinion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rescan on a schedule, alert on regression&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, on the machine it is installed on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where the extension stops
&lt;/h2&gt;

&lt;p&gt;It audits the origin of the tab you have open, on the machine it is installed on, while the browser is running. Scheduled rescans go through a bounded batch per cycle, so a watch list is a handful of sites you care about, not an inventory.&lt;/p&gt;

&lt;p&gt;For getting a site agent-ready before you publish it, that is the whole job, and it costs nothing. The point where it stops is the point where the question changes from "is this site ready" to "are all of them still ready" — every domain in a portfolio, checked on a schedule that does not depend on someone's laptop being awake, with the alert going to whoever is on call. That is the same boundary the &lt;a href="https://301.sh/see-the-redirect-chain-your-browser-followed/" rel="noopener noreferrer"&gt;redirect chain recorder&lt;/a&gt; runs into, for the same reason, and it is what &lt;a href="https://301.st/features" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; does across a portfolio. For the site in front of you, install the extension and read the evidence.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>browser</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A unique click id on a free Cloudflare redirect, no Worker involved</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Thu, 06 Aug 2026 14:00:21 +0000</pubDate>
      <link>https://dev.to/301st/a-unique-click-id-on-a-free-cloudflare-redirect-no-worker-involved-1750</link>
      <guid>https://dev.to/301st/a-unique-click-id-on-a-free-cloudflare-redirect-no-worker-involved-1750</guid>
      <description>&lt;p&gt;You send paid or partner traffic through a redirect and you want every click to arrive at the destination carrying its own identifier — something to join against a conversion later. The obvious tool is a Worker, and for anything that needs to &lt;em&gt;record&lt;/em&gt; the click, a Worker &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;is still the only primitive that can&lt;/a&gt;. But if all you need is for each click to &lt;em&gt;carry&lt;/em&gt; a unique id, there is a way to get one on the free plan, inside a plain Single Redirect, with no code in the request path at all.&lt;/p&gt;

&lt;p&gt;We tested it on this site's zone on 30 July 2026. It works, and three of its edges bite. Here is the rule, the measurements, and the traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The function that is banned, and the field that is not
&lt;/h2&gt;

&lt;p&gt;The ruleset expression language has &lt;code&gt;uuidv4()&lt;/code&gt;. You cannot use it here. The documentation restricts it to rewrite expressions of Transform Rules, and the API enforces that at validation time. This is the live answer to an attempt to put it in a redirect target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"'concat(\"https://301.sh/?click_id=\", uuidv4(cf.random_seed))' is not a valid
value for target_url because the use of field cf.random_seed is not allowed,
the use of function uuidv4 is not allowed" (code 20083)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the function and its seed field are named as forbidden. So the expression language cannot &lt;em&gt;mint&lt;/em&gt; randomness in a redirect. What it can do is reuse an identifier Cloudflare has already minted for the request: &lt;code&gt;cf.ray_id&lt;/code&gt;, the ray ID that every request through Cloudflare gets, the same value the &lt;code&gt;CF-RAY&lt;/code&gt; response header and the dashboard logs show. Its field documentation carries no product restriction, and the validator accepts it in a redirect target.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule, measured
&lt;/h2&gt;

&lt;p&gt;A dynamic Single Redirect. The match is ordinary; the target is an expression instead of a static URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expression:  (http.host eq "301.sh" and http.request.uri.path eq "/go")
Target URL:  concat("https://destination.example/?click_id=", cf.ray_id)
Status:      302, preserve query string off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five requests in a row through that rule, on this zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Location: https://301.sh/…/?click_id=a23684c8a885b183-BTS   CF-RAY: a23684c8a885b183-BTS
Location: https://301.sh/…/?click_id=a23684cbfd5a2915-BTS   CF-RAY: a23684cbfd5a2915-BTS
Location: https://301.sh/…/?click_id=a23684cf493d696b-BTS   CF-RAY: a23684cf493d696b-BTS
Location: https://301.sh/…/?click_id=a23684d2ddcf1948-BTS   CF-RAY: a23684d2ddcf1948-BTS
Location: https://301.sh/…/?click_id=a23684d64c0a46d5-BTS   CF-RAY: a23684d64c0a46d5-BTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties, all visible in the output. Every request got a different id. Each id is identical, byte for byte, to the &lt;code&gt;CF-RAY&lt;/code&gt; header of the same response — which means the click id in your destination's logs joins directly against Cloudflare's own logs and dashboard. And the substituted value is the full form with the data center suffix (&lt;code&gt;-BTS&lt;/code&gt; here), not just the sixteen hex characters, so parse accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preserve_query_string trap
&lt;/h2&gt;

&lt;p&gt;The obvious next step is to keep the campaign parameters the click arrived with. The rule has a switch for that, and combining it with a target expression that already carries a query is where it breaks — measured, not guessed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Incoming request&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Location&lt;/code&gt; actually sent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preserve_query_string: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/go?utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;…/?utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt; — click_id &lt;strong&gt;gone&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preserve_query_string: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;…/&lt;/code&gt; — no query at all, even the target's own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;manual merge, flag off&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/go?utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;…/?click_id=a2368ff0…-BTS&amp;amp;utm_source=tg&amp;amp;gclid=abc123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;preserve_query_string&lt;/code&gt; does not merge the incoming query into your target. It replaces the target's query entirely — including when the incoming query is empty, which deletes the &lt;code&gt;click_id&lt;/code&gt; you just built. The two features are mutually exclusive.&lt;/p&gt;

&lt;p&gt;The working form leaves the switch off and does the merge in the expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concat("https://destination.example/?click_id=", cf.ray_id, "&amp;amp;", http.request.uri.query)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One cosmetic edge: when the incoming query is empty, the result ends in a bare &lt;code&gt;&amp;amp;&lt;/code&gt;. Every query parser we care about ignores it, but it is in the URL, so know it is there before you diff logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 301 will replay yesterday's id
&lt;/h2&gt;

&lt;p&gt;The same rule with status 301 substitutes a fresh ray ID per request at the edge — we measured that too. The problem is in front of the edge: the 301 comes back with no &lt;code&gt;Cache-Control&lt;/code&gt; header, and a permanent redirect without explicit freshness information is exactly what browsers are allowed to cache heuristically. A repeat visitor can then be redirected by their own cache, carrying the click id from their first visit, and the request never reaches Cloudflare at all — a duplicate id in your logs and an undercounted click. For this pattern the status you want is 302 or 307.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nothing on Cloudflare's side records the id.&lt;/strong&gt; Redirect is a terminating action in the first request phase; &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;no analytics product ever sees the request&lt;/a&gt;, and the rule cannot write anywhere. The id exists only in the &lt;code&gt;Location&lt;/code&gt; URL. If the destination does not log its query string, the id evaporates in flight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A ray ID is an identifier, not a secret.&lt;/strong&gt; It is unique, but it is not random in any adversarial sense — do not use it as a token that authorizes anything. Join key: yes. Capability: no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every fetch gets an id, not every human.&lt;/strong&gt; Link prefetchers, scanners and preview bots follow redirects too, and each gets its own perfectly valid click id. The rule cannot tell them apart; dedup and filtering stay the destination's job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The quota is real but roomy.&lt;/strong&gt; Ten Single Redirect rules per zone on the free plan — per zone, not per account — then 25 on Pro, 50 on Business, 300 on Enterprise (checked 30 July 2026). One tracking rule with a wildcard covers a whole path family, so ten goes further than it sounds. Regular expressions need Business; this pattern does not need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need more than a stamp
&lt;/h2&gt;

&lt;p&gt;If the destination is yours, this rule plus your own analytics reading &lt;code&gt;click_id&lt;/code&gt; may be the whole system: no Worker, no code, nothing to maintain, and the id joins against Cloudflare's logs by construction.&lt;/p&gt;

&lt;p&gt;The pattern stops being enough exactly where the &lt;a href="https://301.sh/attribution-when-the-conversion-page-is-not-yours/" rel="noopener noreferrer"&gt;attribution article&lt;/a&gt; starts: the destination is not yours, or you need the click recorded even when the destination logs nothing, or you run this across a portfolio of domains rather than one zone. Recording at the redirect side is precisely the thing a terminating rule cannot do — that side needs to be operated, which is what &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; is for: it owns the redirect end, mints and records the id there, and hands the same id downstream, so the join works whether or not the destination cooperates.&lt;/p&gt;

</description>
      <category>tracking</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Every job your VPS does, and what Cloudflare's free plan does with it</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Wed, 05 Aug 2026 14:00:26 +0000</pubDate>
      <link>https://dev.to/301st/every-job-your-vps-does-and-what-cloudflares-free-plan-does-with-it-5eao</link>
      <guid>https://dev.to/301st/every-job-your-vps-does-and-what-cloudflares-free-plan-does-with-it-5eao</guid>
      <description>&lt;p&gt;The fear of leaving a VPS is rarely about any one feature. A small server quietly does ten jobs at once — serves a site, renews certificates, answers DNS, redirects old URLs, runs a cron script, forwards mail, keeps a database, stores uploads, hosts a small API, rate limits a scraper — and the migration question is really ten questions asked as one. Ask "can Cloudflare replace my VPS" and you get advocacy in both directions. Ask it per job and you get answers you can check.&lt;/p&gt;

&lt;p&gt;So here is the job by job mapping, every number read off the live documentation on 29 July 2026, with the allowance, the price of lifting it, and the honest rows where the answer is "keep the server." At the end: two systems that already live this way, one of them a production SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mapping
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VPS job&lt;/th&gt;
&lt;th&gt;On the free plan&lt;/th&gt;
&lt;th&gt;The allowance&lt;/th&gt;
&lt;th&gt;Where it bites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serve a static site&lt;/td&gt;
&lt;td&gt;Workers static assets&lt;/td&gt;
&lt;td&gt;asset requests "free and unlimited"; 20,000 files, 25 MiB each&lt;/td&gt;
&lt;td&gt;paths routed through Worker code count against 100,000 requests a day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS certificates&lt;/td&gt;
&lt;td&gt;Universal SSL&lt;/td&gt;
&lt;td&gt;90 day certs, auto renewed from 30 days out&lt;/td&gt;
&lt;td&gt;proxied hostnames only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;free, with unmetered DDoS protection&lt;/td&gt;
&lt;td&gt;200 records per zone (zones created since September 2024; older free zones keep 1,000)&lt;/td&gt;
&lt;td&gt;nameservers must move to Cloudflare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirects&lt;/td&gt;
&lt;td&gt;Single + Bulk Redirects&lt;/td&gt;
&lt;td&gt;10 rules; 10,000 bulk URLs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;the bulk quota may lag in your account&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron scripts&lt;/td&gt;
&lt;td&gt;Cron Triggers on a Worker&lt;/td&gt;
&lt;td&gt;5 per account documented, minimum interval 1 minute&lt;/td&gt;
&lt;td&gt;see the next section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small APIs, dynamic bits&lt;/td&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;100,000 requests a day, 10 ms CPU each&lt;/td&gt;
&lt;td&gt;computing breaks the CPU budget; serving does not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key value storage&lt;/td&gt;
&lt;td&gt;Workers KV&lt;/td&gt;
&lt;td&gt;1 GB, 100,000 reads / 1,000 writes a day&lt;/td&gt;
&lt;td&gt;1,000 writes is less than it sounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A real database&lt;/td&gt;
&lt;td&gt;D1 (SQLite)&lt;/td&gt;
&lt;td&gt;5 GB, 5 million reads / 100,000 writes a day&lt;/td&gt;
&lt;td&gt;daily caps error out, not throttle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;R2&lt;/td&gt;
&lt;td&gt;10 GB, free egress&lt;/td&gt;
&lt;td&gt;free tier covers Standard storage only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mail forwarding&lt;/td&gt;
&lt;td&gt;Email Routing&lt;/td&gt;
&lt;td&gt;free on all plans; 200 rules, 200 destinations&lt;/td&gt;
&lt;td&gt;forwarding, not sending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall, rate limits&lt;/td&gt;
&lt;td&gt;WAF free tier&lt;/td&gt;
&lt;td&gt;5 custom rules, 1 rate limiting rule&lt;/td&gt;
&lt;td&gt;one rate limit means choosing what to protect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request logs&lt;/td&gt;
&lt;td&gt;Workers Logs, Analytics Engine&lt;/td&gt;
&lt;td&gt;logs with sampling; 100,000 data points a day&lt;/td&gt;
&lt;td&gt;Logpush is Enterprise; Workers Trace Events unlock at $5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of those rows deserve more than a cell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cron entry that should not fire, and does
&lt;/h2&gt;

&lt;p&gt;The documentation says the free plan allows 5 Cron Triggers per account. The account this blog runs on executes 7, every day, across five distribution Workers — and has for a week, posting to five platforms on schedule. We did not negotiate anything; the limit simply is not enforced against this account.&lt;/p&gt;

&lt;p&gt;That is not a loophole to build on. It is the same phenomenon we documented when &lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;the Bulk Redirects quota said 10,000 and dashboards enforced 20&lt;/a&gt;: Cloudflare's documented numbers and per account entitlements drift apart, in both directions, sometimes for months. Read the number, then test the number. On a migration that habit costs you an afternoon and saves you an architecture built around a limit that was never real for you — or one that assumed an allowance you do not actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Certificates are the quiet argument
&lt;/h2&gt;

&lt;p&gt;On a VPS the TLS routine was a yearly renewal, or a certbot timer. That era is ending on a schedule: since 15 March 2026 the CA/Browser Forum rules cap public certificates at 200 days, in March 2027 the cap drops to 100 days, and in March 2029 to 47 days with domain validation reuse down to 10 days. Manual renewal stops being a viable habit and becomes an incident generator — a portfolio of parked domains on a VPS meets that wave at its first short renewal this autumn.&lt;/p&gt;

&lt;p&gt;Behind the proxy, Universal SSL already issues 90 day certificates and renews them starting 30 days out, which sits below every step of that schedule. Of everything in the table, this is the row where the free plan is not a cheaper version of the VPS job but a categorically better one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceiling is real, and it is measurable
&lt;/h2&gt;

&lt;p&gt;The 10 ms CPU budget is the free plan's one hard wall, and we have measured both sides of it on this site. A Worker that serves — &lt;a href="https://301.sh/mcp-server-on-workers-free-plan/" rel="noopener noreferrer"&gt;our MCP server&lt;/a&gt; answering from static assets — spends 0 to 2 ms of the budget, because waiting on I/O does not count as CPU. A Worker that computes — the template engine we measured in &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;the agent readiness audit&lt;/a&gt; — blew through the same budget by a factor of eight. That is the honest split for a migration: routing, redirecting, serving, forwarding all fit with room to spare; parsing, rendering and hashing at volume do not.&lt;/p&gt;

&lt;p&gt;Per this site's house rule, the price of the lift sits next to the limit: the Workers Paid plan at $5 a month raises the per request cap to 30 seconds by default and includes 10 million requests and 30 million CPU milliseconds a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What honestly stays on the server
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long running processes.&lt;/strong&gt; Anything that holds a socket open for hours, speaks a protocol that is not HTTP, or needs SSH. Arbitrary TCP proxying exists (Spectrum) but only as an Enterprise extra — that is not a migration, that is procurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy compute&lt;/strong&gt;, per the measurement above, unless $5 and 30 seconds cover it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The paid tier gaps.&lt;/strong&gt; Snippets are absent from Free entirely, Logpush is Enterprise, Load Balancing and Argo are paid extras. If your VPS job maps to one of these, the free plan answer is "no", not "yes with effort".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control you did not know you had.&lt;/strong&gt; On Cloudflare Registrar you cannot change nameservers at all, and &lt;a href="https://301.sh/auto-renew-on-domain-still-expired/" rel="noopener noreferrer"&gt;renewal is an instruction, not a guarantee&lt;/a&gt;. A migration hands real levers to one vendor; that is a decision, not a detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And there is a bridge for the undecided: Cloudflare Tunnel is available on all plans, so the VPS can stay exactly where it is, hidden behind the edge with no open ports, while jobs migrate off it one row at a time. Nothing about this table is all or nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two systems that already live like this
&lt;/h2&gt;

&lt;p&gt;This blog is the small case: six Workers — the site plus five distribution posters — seven cron firings a day, a shared KV namespace, 317 static files, an MCP server, at a running cost of exactly zero, with the measurements published as articles.&lt;/p&gt;

&lt;p&gt;The larger case is &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt;, the platform this blog belongs to: a production SaaS for domains, redirects and traffic distribution, built as a serverless application on Cloudflare Workers with D1 as the source of truth and KV for cache and sessions. Its architecture documentation states the design goal plainly: customers do not need a paid Cloudflare plan — everything runs on free Workers, and Workers Paid is the growth path, not the entry fee. A platform whose job is other people's domains chose the same table you just read. That is as honest an endorsement of the free plan as we can offer. And where a portfolio outgrows the rows you can run yourself, taking that work over is exactly what the platform exists for.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>dns</category>
      <category>webdev</category>
    </item>
    <item>
      <title>An MCP server on Cloudflare's free plan, measured against the 10 ms CPU limit</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:00:54 +0000</pubDate>
      <link>https://dev.to/301st/an-mcp-server-on-cloudflares-free-plan-measured-against-the-10-ms-cpu-limit-468b</link>
      <guid>https://dev.to/301st/an-mcp-server-on-cloudflares-free-plan-measured-against-the-10-ms-cpu-limit-468b</guid>
      <description>&lt;p&gt;Two things happened this week that turn "can I run an MCP server for free" from a compromise into a plain yes, with one measurable condition. Today the Model Context Protocol shipped its &lt;code&gt;2026-07-28&lt;/code&gt; revision, and the headline change is that the protocol core went stateless: sessions, the &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header, and the &lt;code&gt;initialize&lt;/code&gt; handshake are gone. The release post states the consequence outright:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any request can now land on any server instance behind a plain round-robin load balancer without needing shared storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yesterday, one day ahead of the release, Cloudflare's own documentation flipped. The &lt;code&gt;McpAgent&lt;/code&gt; class — the official path that backed every MCP server with a Durable Object because the transport needed somewhere to keep its session — is now marked deprecated and feature frozen, with a stateless request handler as the recommended replacement for new servers.&lt;/p&gt;

&lt;p&gt;Those two moves close the same gap from both ends. A Worker on the free plan was always a natural place for a small MCP server except for one thing: the transport demanded state, and state meant a Durable Object and a sticky instance. Now the protocol itself promises that every request is self contained. What remains is the free plan's one hard ceiling — 10 ms of CPU per request — and whether a real server fits under it is not a question you argue about. It is a question you measure. So we built one, on this site, and measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the revision actually removed
&lt;/h2&gt;

&lt;p&gt;The old lifecycle opened every connection with an &lt;code&gt;initialize&lt;/code&gt; round trip, minted a session id, and required the client to carry it on every call. All of that is deleted. Version, client identity, and capabilities now travel inside each request's &lt;code&gt;_meta&lt;/code&gt; fields, and the one thing a server must implement is &lt;code&gt;server/discover&lt;/code&gt;, which reports its supported versions and capabilities to anyone who asks.&lt;/p&gt;

&lt;p&gt;The transport got stricter in exchange. Every POST must carry an &lt;code&gt;MCP-Protocol-Version&lt;/code&gt; header that matches the version inside the body, plus an &lt;code&gt;Mcp-Method&lt;/code&gt; header mirroring the JSON-RPC method — and &lt;code&gt;Mcp-Name&lt;/code&gt; on tool calls — so load balancers and gateways can route without parsing bodies. A mismatch is a hard &lt;code&gt;400&lt;/code&gt;. Batching stays gone, &lt;code&gt;ping&lt;/code&gt; is removed entirely, an unknown method is now an HTTP &lt;code&gt;404&lt;/code&gt;, and the 2024 era HTTP+SSE transport is formally classified as deprecated. A server that never pushes messages may answer every request with plain JSON and refuse the streaming path with a &lt;code&gt;405&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a server that only reads, the protocol surface left over is small: &lt;code&gt;server/discover&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;, and the header discipline. That is the whole list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server this site now runs
&lt;/h2&gt;

&lt;p&gt;This blog already publishes an agent surface — Markdown mirrors of every article and a &lt;code&gt;posts.json&lt;/code&gt; feed. The MCP server is those same files behind two tools: &lt;code&gt;list_articles&lt;/code&gt; returns the feed, &lt;code&gt;get_article&lt;/code&gt; returns an article's full Markdown. It lives at &lt;code&gt;https://301.sh/mcp&lt;/code&gt; on the same Worker that does the site's &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;content negotiation&lt;/a&gt;, with zero dependencies — after the revision, the remaining protocol surface is short enough to write out by hand. Cloudflare ships an SDK route for the same job (&lt;code&gt;createMcpHandler&lt;/code&gt; plus the protocol SDK), which is the right answer the moment you need resources, prompts, or elicitation; a two tool read only server does not.&lt;/p&gt;

&lt;p&gt;The dispatch, condensed to its shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server/discover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resultType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;supportedVersions&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;2026-07-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&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="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resultType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/call&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="c1"&gt;// Mcp-Name header must equal params.name, or 400 + HeaderMismatch.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mirror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resultType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mirror&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&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="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Method not found: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in it computes. Every answer is an already deployed static asset fetched through the assets binding, and that choice is exactly what the measurement rewards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;Cloudflare's definition of the limit is the part most people skip past:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CPU time measures how long the CPU spends executing your Worker code. Waiting on network requests (such as &lt;code&gt;fetch()&lt;/code&gt; calls, KV reads, or database queries) does &lt;strong&gt;not&lt;/strong&gt; count toward CPU time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the 10 ms budget is spent on parsing, validating, and assembling JSON — not on fetching the article body. We sent 40 requests at the production endpoint and read the per invocation &lt;code&gt;cpuTime&lt;/code&gt; that Workers Logs record:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;CPU, ms (min / median / max)&lt;/th&gt;
&lt;th&gt;Wall, ms (min / median / max)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;server/discover&lt;/td&gt;
&lt;td&gt;0 / 0 / 1&lt;/td&gt;
&lt;td&gt;0 / 1 / 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tools/list&lt;/td&gt;
&lt;td&gt;0 / 0 / 0&lt;/td&gt;
&lt;td&gt;0 / 1 / 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list_articles&lt;/td&gt;
&lt;td&gt;0 / 1 / 1&lt;/td&gt;
&lt;td&gt;10 / 13 / 23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get_article, largest article&lt;/td&gt;
&lt;td&gt;0 / 1 / 2&lt;/td&gt;
&lt;td&gt;6 / 10 / 88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The worst case spends 2 ms of the 10 ms budget, and the slowest response — 88 ms of wall time serving the longest article on the site — is almost entirely waiting on the asset fetch, which the meter ignores. A serving tool runs at a fifth of the free ceiling with room to spare, and the free plan's other number, 100,000 requests a day, is a different class of problem entirely for an endpoint agents call a few times per conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where 10 ms actually bites
&lt;/h2&gt;

&lt;p&gt;The ceiling is real; it just lives somewhere else than the transport. When we &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;audited this site's agent surface&lt;/a&gt;, we measured a template engine we considered exposing as a service, and it blew through the same 10 ms by a factor of eight. That is the honest division: a tool that serves bytes costs one or two milliseconds; a tool that computes — parses documents, renders templates, hashes at scale — eats the budget almost immediately. Cloudflare does allow occasional overruns ("Each isolate has some built-in flexibility"), but a Worker that exceeds the limit consistently is terminated with error 1102, and burst tolerance is not a plan.&lt;/p&gt;

&lt;p&gt;The lift has a price tag, and per this site's house rule it belongs next to the limit: the Workers Paid plan at $5 a month raises the per request cap to a default of 30 seconds (configurable up to 5 minutes) and includes 30 million CPU milliseconds and 10 million requests a month. If your tools compute, that is the number to compare against, not the free tier's 10 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and what this does not need
&lt;/h2&gt;

&lt;p&gt;The endpoint is live, and one request shows the whole new shape of the protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://301.sh/mcp &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'MCP-Protocol-Version: 2026-07-28'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Mcp-Method: server/discover'&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;'{"jsonrpc":"2.0","id":1,"method":"server/discover",
       "params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No session to open, no state to hold, no Durable Object on the bill. If your site already publishes Markdown mirrors or a feed — and after the &lt;a href="https://301.sh/agent-readiness-audit/" rel="noopener noreferrer"&gt;agent readiness audit&lt;/a&gt; ours did — the distance from "static files" to "MCP server" is one route on a Worker you may already be running. The spec finally matches what a small read only server always wanted to be: a plain HTTP endpoint that answers questions about content you have already built. The free plan carries that easily. It is the computing tools that were never going to be free, and now you know the number that decides.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The conversion fires on someone else's domain. Here is how to attribute it.</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:00:53 +0000</pubDate>
      <link>https://dev.to/301st/the-conversion-fires-on-someone-elses-domain-here-is-how-to-attribute-it-37ae</link>
      <guid>https://dev.to/301st/the-conversion-fires-on-someone-elses-domain-here-is-how-to-attribute-it-37ae</guid>
      <description>&lt;p&gt;You buy the traffic, the sale happens on a domain you cannot touch. An affiliate offer, a product page on a marketplace, a partner's checkout, any program where the "thank you" page belongs to the platform. You cannot place a pixel there, and the ad platform is optimizing blind: the network dashboard shows dozens of conversions, the ad account registers a handful, and the algorithm steers toward clicks, because clicks are all it can see.&lt;/p&gt;

&lt;p&gt;The question surfaces in PPC and affiliate communities year after year, asked about Meta traffic to marketplaces, about ClickBank offers, about white label programs, and the public answers have not moved in a decade. They come in two kinds: "you cannot implement anything on their side", which is true, and "buy a tracker", which starts at three figures a month. At least one asker gave up and publicly put a €100 bounty on any working idea. Nobody collected.&lt;/p&gt;

&lt;p&gt;The fallback everyone lands on instead is counting clicks on the outbound link as stand in conversions. It rots, because a click is not a sale and bots click too — that €100 was in fact offered for a way to filter bots out of exactly this surrogate metric. Money on the symptom, because the cure was not written down anywhere.&lt;/p&gt;

&lt;p&gt;The "nothing on their side" answer is correct, and it is also not the end. Everything you need can live on yours, in the one place every click still passes through: a redirect hop you own. That pattern is what this article builds — with what it costs, where it breaks, and what the ad platforms allow, checked against their live documentation on 27 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop: click_id out, postback in
&lt;/h2&gt;

&lt;p&gt;The mechanics have been standard in affiliate networks for a decade. What is missing in every one of those threads is someone laying the loop out end to end.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The click leaves your page through a redirect you host: &lt;code&gt;/go&lt;/code&gt;. The hop generates a &lt;code&gt;click_id&lt;/code&gt;, stores it alongside everything you know about the click — the &lt;code&gt;gclid&lt;/code&gt; from the ad, the source, the timestamp — and sends a &lt;code&gt;302&lt;/code&gt; to the network link with the &lt;code&gt;click_id&lt;/code&gt; in the network's subid parameter.&lt;/li&gt;
&lt;li&gt;The network carries your subid through to the sale. When the conversion happens on their side, their system fires a &lt;strong&gt;postback&lt;/strong&gt;: a server to server request to a URL you registered, carrying the subid back, usually with a payout amount.&lt;/li&gt;
&lt;li&gt;Your postback endpoint looks the &lt;code&gt;click_id&lt;/code&gt; up, and now the conversion is joined to the exact click, campaign, and creative that produced it — on your infrastructure, from server logs, with no pixel anywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note what this does not depend on. No JavaScript on the conversion page, because you have no JavaScript there. No cookies surviving anything, because the join key travels inside URLs and server calls. No consent banner in the path, because nothing runs in the visitor's browser beyond the redirect itself. The hop is server side by construction, which is also why ad blockers never see it: the click is a request to your own host before the browser goes anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the hop must not be your ad's final URL
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is to point the ad directly at &lt;code&gt;/go&lt;/code&gt; and skip the landing page. On Google Ads that shortcut is closed twice over, and knowing exactly how saves you a policy strike.&lt;/p&gt;

&lt;p&gt;First, tracking templates do not carry the user anymore. Parallel tracking is mandatory for Search, Shopping, Display, Video, and Performance Max, and Google's description of it is unambiguous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Parallel tracking sends customers directly from your ad to your final URL while click measurement happens in the background (without sending them to the tracking URLs first).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The template URL still gets requested — as a background ping from the browser — so a hop in the tracking template can count the click. But it cannot route the visitor, rewrite where they land, or hand the network a subid on the visit that actually converts.&lt;/p&gt;

&lt;p&gt;Second, making the hop the final URL is a listed policy violation. Google's destination requirements name it directly: "Redirects from the final URL that take the user to a different domain" is a destination mismatch. That is the rule that gets affiliate accounts suspended for direct linking.&lt;/p&gt;

&lt;p&gt;So the compliant shape on Google Ads is a bridge: the final URL is a real page on your domain, and the hop is the outbound link on that page. Make sure the page passes its own query string through to the hop link — the &lt;code&gt;gclid&lt;/code&gt; arrives on the landing page URL thanks to auto tagging, and &lt;a href="https://301.sh/find-the-redirect-that-drops-your-gclid/" rel="noopener noreferrer"&gt;a redirect that drops it&lt;/a&gt; ends attribution before it starts. Meta does not run parallel tracking, but the same bridge shape is where community answers converge for Meta traffic too: a page you own, your pixel on it, subids outbound.&lt;/p&gt;

&lt;p&gt;Closing the loop back into the ad platform is the step everyone skips. On Google Ads it has an official name: offline conversion import. You stored the &lt;code&gt;gclid&lt;/code&gt; next to the &lt;code&gt;click_id&lt;/code&gt; at hop time; when the postback lands, you upload the conversion keyed by that &lt;code&gt;gclid&lt;/code&gt;. Google retains a &lt;code&gt;gclid&lt;/code&gt; for 90 days for this purpose, auto tagging is the only prerequisite, and the feature is current, not legacy. The campaign stops optimizing toward clicks and starts optimizing toward the sales your postbacks confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the hop on Cloudflare's free plan
&lt;/h2&gt;

&lt;p&gt;A redirect rule cannot do this job, and it is worth being precise about why. Single Redirects are a terminating action in the first request phase — &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;nothing downstream ever sees the click&lt;/a&gt;, and the rule itself cannot write anything anywhere. The expression language cannot even mint an id: &lt;code&gt;uuidv4()&lt;/code&gt; exists, but the documentation restricts it to rewrite expressions of Transform Rules. Among &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every way to redirect on Cloudflare&lt;/a&gt;, the only primitive that can mint, store, and answer postbacks is a Worker.&lt;/p&gt;

&lt;p&gt;The whole hop is one Worker with two routes and a D1 table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/go&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO clicks (id, gclid, src, ts) VALUES (?1, ?2, ?3, ?4)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gclid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OFFER_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/postback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POSTBACK_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE clicks SET payout = ?2, converted = ?3 WHERE id = ?1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free plan quotas fit this comfortably, all checked live on 27 July 2026: Workers allows 100,000 requests a day, D1 writes 100,000 rows a day and reads five million — a click and a postback are one row each. Outgrowing them is a billing decision, not a redesign: the Workers Paid plan at $5 a month moves Workers to 10 million requests and D1 to 50 million written rows a month. The one storage choice to get right: this is a lookup workload, not a counting workload, which is why the table is D1 and not Workers KV — KV writes cap at 1,000 a day on Free, a limit that &lt;a href="https://301.sh/count-clicks-on-a-redirect/" rel="noopener noreferrer"&gt;has misled people before&lt;/a&gt;. Registering the postback URL with the network is one form field on their side: your &lt;code&gt;/postback&lt;/code&gt; address with the &lt;code&gt;key&lt;/code&gt; and their subid macro in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;The hop is the reliable half of the loop. The other half belongs to the network, and when the join fails, it fails in one of five recurring ways — this list is assembled from a decade of affiliate forum threads asking why the postback never came:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token name mismatch&lt;/td&gt;
&lt;td&gt;Network expects &lt;code&gt;{clickid}&lt;/code&gt;, you sent &lt;code&gt;{click_id}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Their macro list, character for character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserved subids&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sub1&lt;/code&gt; is taken by the network's own tracking&lt;/td&gt;
&lt;td&gt;Ask which slot is passthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offer strips subids&lt;/td&gt;
&lt;td&gt;Clicks arrive, subid column is empty&lt;/td&gt;
&lt;td&gt;Test link, then check their reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postback registered late&lt;/td&gt;
&lt;td&gt;Conversions before registration are gone for good&lt;/td&gt;
&lt;td&gt;Register before the first paid click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Never tested by hand&lt;/td&gt;
&lt;td&gt;Everything "configured", nothing verified&lt;/td&gt;
&lt;td&gt;Open your postback URL in a browser with a fake subid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the one that catches people who did everything else right. A postback URL you have never fired manually is a guess, not a setup: request it once yourself and watch the row update before any money is spent.&lt;/p&gt;

&lt;p&gt;Two structural limits are worth naming too. If the program offers no postback and no subid reporting at all, no hop can conjure the loop — the join needs their half, and your fallback is reconciling their dashboard against your click log by time and creative, which is exactly the manual matching those community threads end up settling for. And the offline conversion window is real: a sale confirmed more than 90 days after the click cannot be imported against its &lt;code&gt;gclid&lt;/code&gt;, which matters for products with long approval cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Worker is enough, and when it stops being
&lt;/h2&gt;

&lt;p&gt;For one offer and one traffic source, the Worker above genuinely closes the €100 question: every click logged with its &lt;code&gt;gclid&lt;/code&gt;, every conversion joined server side, the ad platform fed real sales instead of proxy clicks. Run it and stop paying for attribution you can build in an afternoon.&lt;/p&gt;

&lt;p&gt;The pattern stops being an afternoon when it multiplies. Ten offers mean ten target URLs and ten postback registrations; three traffic sources mean the hop has to split by source; a dead offer means rerouting yesterday's links without breaking yesterday's subids. At that point the hop is not a script anymore, it is a routing layer with a database behind it — which is the thing &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; already is: streams that route each click by rules, &lt;code&gt;click_id&lt;/code&gt; minted and logged on every hop, postbacks received and matched as the default path, not as your weekend project. For a single campaign, keep the Worker. It is honest work and it is yours.&lt;/p&gt;

</description>
      <category>tracking</category>
      <category>redirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to see the redirect chain your browser actually followed</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:00:53 +0000</pubDate>
      <link>https://dev.to/301st/how-to-see-the-redirect-chain-your-browser-actually-followed-1ifm</link>
      <guid>https://dev.to/301st/how-to-see-the-redirect-chain-your-browser-actually-followed-1ifm</guid>
      <description>&lt;p&gt;A link lands somewhere it should not, and you need to know what happened between the click and the final page. The standard first move is &lt;code&gt;curl&lt;/code&gt;, and it is the right one: the &lt;a href="https://301.sh/find-the-redirect-that-drops-your-gclid/" rel="noopener noreferrer"&gt;60 second gclid test&lt;/a&gt; on this site is built on &lt;code&gt;curl -sIL&lt;/code&gt;, and for server redirects it tells the truth. Then the chain looks clean in the terminal while the browser still ends up somewhere else, and the tool has quietly run out.&lt;/p&gt;

&lt;p&gt;The reason is that only some redirects live in HTTP responses. The rest happen inside the page, or inside the browser itself, and no amount of &lt;code&gt;-L&lt;/code&gt; will surface them. This article is about seeing a chain, not building one; every way to create a redirect on Cloudflare is &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;its own comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three redirects curl cannot show you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A JavaScript redirect is a 200.&lt;/strong&gt; The server answers with a normal page, the page runs &lt;code&gt;location.replace()&lt;/code&gt; or sets &lt;code&gt;location.href&lt;/code&gt;, and the browser navigates. &lt;code&gt;curl&lt;/code&gt; prints &lt;code&gt;HTTP/2 200&lt;/code&gt;, reports no redirect at all, and exits. Every interstitial, every tracking hop built on a script, every "checking your browser" page falls in this class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A meta refresh is also a 200.&lt;/strong&gt; &lt;code&gt;&amp;lt;meta http-equiv="refresh" content="0;url=..."&amp;gt;&lt;/code&gt; sits in the HTML body, and &lt;code&gt;curl&lt;/code&gt; does not parse HTML. Same clean status line, same silent navigation in the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An HSTS upgrade never leaves the browser.&lt;/strong&gt; After one visit to a site that sends &lt;code&gt;Strict-Transport-Security&lt;/code&gt;, the browser rewrites &lt;code&gt;http://&lt;/code&gt; to &lt;code&gt;https://&lt;/code&gt; before any request is made. DevTools shows it as &lt;code&gt;307 Internal Redirect&lt;/code&gt;, a response the server never sent. &lt;code&gt;curl&lt;/code&gt; keeps no HSTS state by default, so it hits the &lt;code&gt;http://&lt;/code&gt; URL fresh every time and shows you the server's &lt;code&gt;301&lt;/code&gt;, a hop your returning visitors stopped taking long ago.&lt;/p&gt;

&lt;p&gt;On top of the mechanics there is the session. &lt;code&gt;curl&lt;/code&gt; carries no cookies, so a chain that branches on login state, a consent cookie, or an A/B assignment sends &lt;code&gt;curl&lt;/code&gt; down a different path than the person who reported the problem. The chain you tested is not the chain they followed, even when both are real.&lt;/p&gt;

&lt;h2&gt;
  
  
  DevTools shows it, if you hold it right
&lt;/h2&gt;

&lt;p&gt;The browser obviously knows the full chain, and the Network panel will show it, with ceremony. Preserve log has to be on, otherwise the panel clears itself on every navigation and the hop you care about erases its own evidence. The Doc filter cuts the noise from the hundred subrequests a modern page makes. Server hops then appear as separate rows with their &lt;code&gt;301&lt;/code&gt; and &lt;code&gt;302&lt;/code&gt; statuses.&lt;/p&gt;

&lt;p&gt;The client side hops are there too, but unlabeled. A JavaScript redirect is just another document request; nothing in the row says "this was a redirect", and connecting it to the page that triggered it means reading the Initiator column. A meta refresh looks the same. Do this once and it is five minutes of archaeology. Do it for every report and you start wanting the browser to just keep the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  A recorder instead of a stakeout
&lt;/h2&gt;

&lt;p&gt;That is what &lt;a href="https://chromewebstore.google.com/detail/redirect-inspector/jkeijlkbgkdnhmejgofbbapdbhjljdgg" rel="noopener noreferrer"&gt;Redirect Inspector&lt;/a&gt; does. It is our own open source extension (&lt;a href="https://github.com/investblog/redirect-inspector" rel="noopener noreferrer"&gt;Apache 2.0, code on GitHub&lt;/a&gt;), and version 2.3 shipped as its largest update yet, which is the occasion for this article. The extension listens to the browser's own request pipeline and records every chain as you browse: server hops with their status codes, client side navigations (a script or a meta refresh) marked with a &lt;code&gt;JS&lt;/code&gt; badge, and the browser's internal upgrades marked &lt;code&gt;HSTS&lt;/code&gt;. Chains are grouped by browsing session, tracking pixels and ad beacons are filtered out by default, and each card carries the total chain time with per hop timing in the analysis drawer.&lt;/p&gt;

&lt;p&gt;What 2.3 added, briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check a URL without visiting it.&lt;/strong&gt; Paste a URL into the dock at the bottom of the panel; it opens in an invisible background tab, the chain is captured through the normal pipeline, and the tab closes itself. The suspicious link from the report gets traced without ever taking over your screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export that fits a bug report.&lt;/strong&gt; Copy a clean summary, download the raw chain as JSON, or copy a ready &lt;code&gt;curl&lt;/code&gt; command that reproduces the server side of the chain, which closes the loop back to the terminal where the investigation started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A side panel instead of a popup&lt;/strong&gt; on Chrome and Edge, so the chain list stays open while you browse. Firefox keeps the popup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local analysis.&lt;/strong&gt; Nine checks per chain: loops, hops that bounce back and forth, chain length, mixed redirect types, auth bounces, consent hops, tracking noise, CDN detection, final outcome.&lt;/li&gt;
&lt;li&gt;Search across all captured URLs, an undoable Clear, keyboard shortcuts, and full UI translations in seven languages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part that matters for a tool watching your traffic: everything runs locally. No accounts, no analytics, no telemetry, and by default no network requests at all; captured chains live in browser storage and never leave the machine. It is on the &lt;a href="https://chromewebstore.google.com/detail/redirect-inspector/jkeijlkbgkdnhmejgofbbapdbhjljdgg" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;, &lt;a href="https://addons.mozilla.org/firefox/addon/redirect-inspector/" rel="noopener noreferrer"&gt;Firefox Add-ons&lt;/a&gt;, and &lt;a href="https://microsoftedge.microsoft.com/addons/detail/ckblhiaefgkhpgilekhcpapnkpihdlaa" rel="noopener noreferrer"&gt;Edge Add-ons&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tool sees what
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;curl -sIL&lt;/th&gt;
&lt;th&gt;DevTools Network&lt;/th&gt;
&lt;th&gt;Redirect Inspector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server hops (301, 302, 307, 308)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;with Preserve log on&lt;/td&gt;
&lt;td&gt;yes, recorded as you browse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HSTS upgrade&lt;/td&gt;
&lt;td&gt;no, sees the server 301 every time&lt;/td&gt;
&lt;td&gt;shown as 307 Internal Redirect&lt;/td&gt;
&lt;td&gt;labeled HSTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS and meta refresh&lt;/td&gt;
&lt;td&gt;no, both are a 200&lt;/td&gt;
&lt;td&gt;an unlabeled document request&lt;/td&gt;
&lt;td&gt;captured, marked as a client redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The chain your session gets (cookies, login)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;one total with &lt;code&gt;-w&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;per request, spread across rows&lt;/td&gt;
&lt;td&gt;total per chain, delta per hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A record you can search and export later&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;cleared when the panel closes&lt;/td&gt;
&lt;td&gt;sessions, search, JSON and curl export&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Online redirect checkers are missing from the table on purpose: they request the URL from their own datacenter, so a chain that branches on country, language, or cookies shows you the datacenter's version, which is the same blind spot &lt;code&gt;curl&lt;/code&gt; has plus somebody else's IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the extension stops
&lt;/h2&gt;

&lt;p&gt;The extension records what &lt;strong&gt;your&lt;/strong&gt; browser was sent. A chain that branches by geography still shows you the branch for your country and nobody else's, and what Googlebot sees on the same URL is a different question entirely. The URL check is a real navigation in an inactive tab, so your cookies apply and the visit lands in your history; it is a convenience, not an isolation chamber. And it can only look at chains one browser at a time, on the machine where it is installed.&lt;/p&gt;

&lt;p&gt;For debugging the link in front of you, that is the whole job, and the extension is free. The point where it stops is the point where the question changes from "what happened to this click" to "which of my domains are redirecting wrong right now". Answering that means sending the same probe to every domain on a schedule and comparing against yesterday, which no browser tool can do. The &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt; is the manual version of that answer, and &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; is the version that runs continuously, across the portfolio, with an alert when a chain changes. For the tab you have open, install the extension and read the chain.&lt;/p&gt;

</description>
      <category>redirects</category>
      <category>browser</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Redirecting domains you own to one site, without tripping Google's spam rules</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:00:16 +0000</pubDate>
      <link>https://dev.to/301st/redirecting-domains-you-own-to-one-site-without-tripping-googles-spam-rules-1d5l</link>
      <guid>https://dev.to/301st/redirecting-domains-you-own-to-one-site-without-tripping-googles-spam-rules-1d5l</guid>
      <description>&lt;p&gt;You own a small pile of domains — an old brand you moved away from, a couple of typo variants you grabbed so nobody else would, a name from a project that folded — and you want them all pointing at your current site. Then you read that redirecting domains is a spam signal now, that Google's latest update torched sites for doing exactly that, and you stop with your hand on the button.&lt;/p&gt;

&lt;p&gt;Two different things are being run together under one scary headline. One of them Google is actively penalizing. The other Google names, in its own policy, as a legitimate reason to redirect. The whole question is which one you are doing — and the line is clearer than the headlines make it sound. This is the SEO side of the &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;parked-domain runbook&lt;/a&gt; and of &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every way to redirect on Cloudflare&lt;/a&gt;: the same &lt;code&gt;301&lt;/code&gt;, and whether Google honours it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google actually went after
&lt;/h2&gt;

&lt;p&gt;The thing that got hit has a name in Google's spam policies:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expired domain abuse is where an expired domain name is purchased and repurposed primarily to manipulate search rankings by hosting content that provides little to no value to users.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the play: buy a dropped domain that still carries ranking signals from its old life, point it at your site, and inherit authority you never earned. Google's March 2026 spam update did not invent a rule for this — expired domain abuse was codified back in 2024 — it enforced the existing one harder and faster. The market felt it. The advice in the expired-domain trade has visibly moved from hunting aged domains to starting fresh ones, and the sites that leaned hardest on authority-by-redirect are the ones that lost traffic. If the plan was to buy a stranger's old authority and &lt;code&gt;301&lt;/code&gt; it into your rankings, that plan is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google still blesses
&lt;/h2&gt;

&lt;p&gt;Now the other thing. Redirecting a domain you own to a site you own, because the two belong together, is not on any spam list. It is the opposite: Google's own policy on sneaky redirects lists consolidating pages among the legitimate reasons to redirect, right next to moving to a new address. What makes a redirect sneaky is intent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When examining if a redirect is sneaky, consider whether or not the redirect is intended to deceive either the users or search engines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is no deception in sending an old brand's domain to that brand's current home. The mechanics agree: a &lt;code&gt;301&lt;/code&gt; to a topically related destination still passes its signals, the way it has for years. Where it stops working is relevance. A &lt;code&gt;301&lt;/code&gt; from an unrelated domain gets treated like a soft &lt;code&gt;404&lt;/code&gt;, and Google ignores the equity rather than handing it over. That is the tell for the whole distinction — a relevant consolidation is a redirect Google honours, an unrelated authority grab is a redirect Google declines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one are you doing
&lt;/h2&gt;

&lt;p&gt;The line, as a handful of questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Consolidation — do it&lt;/th&gt;
&lt;th&gt;Authority grab — dead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whose domain&lt;/td&gt;
&lt;td&gt;yours, related to the target&lt;/td&gt;
&lt;td&gt;bought for its old rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The link between them&lt;/td&gt;
&lt;td&gt;same brand, product, or topic&lt;/td&gt;
&lt;td&gt;unrelated to your site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the redirect is for&lt;/td&gt;
&lt;td&gt;keeping your own traffic and name&lt;/td&gt;
&lt;td&gt;inheriting a stranger's authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it points&lt;/td&gt;
&lt;td&gt;the page that replaces the old one&lt;/td&gt;
&lt;td&gt;your homepage, to pass equity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google's verdict&lt;/td&gt;
&lt;td&gt;legitimate, passes signals&lt;/td&gt;
&lt;td&gt;expired domain abuse, or ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If every answer sits in the left column, you are doing the thing Google's policy names as fine, and the spam update is not about you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it cleanly on Cloudflare
&lt;/h2&gt;

&lt;p&gt;Being on the right side of the line is most of the job. Doing it cleanly is the rest, and a few Cloudflare-specific choices keep a legitimate consolidation from reading like a lazy one.&lt;/p&gt;

&lt;p&gt;Point each old URL at the page that replaces it, not at your homepage. A blanket redirect of everything to the root is what muddies your anchor-text profile and looks like a link grab even when it isn't; where the old domain had real pages, map them to their real equivalents. The &lt;a href="https://301.sh/redirect-200-parked-domains/" rel="noopener noreferrer"&gt;parked-domain runbook&lt;/a&gt; has the Cloudflare pattern for a domain with no server behind it — a proxied &lt;code&gt;A&lt;/code&gt; record on &lt;code&gt;192.0.2.1&lt;/code&gt; and a redirect rule — and &lt;a href="https://301.sh/cloudflare-free-plan-redirect-limits/" rel="noopener noreferrer"&gt;Bulk Redirects&lt;/a&gt; is where a whole portfolio lives: one account-level list across every domain, rather than a rule per zone.&lt;/p&gt;

&lt;p&gt;Keep it a single &lt;code&gt;301&lt;/code&gt;. Redirect chains — the old domain to a second domain to the site — leak signal at every hop and are slower for the visitor, and there is no reason to build one when the destination is fixed. One permanent hop, straight to the relevant page.&lt;/p&gt;

&lt;p&gt;One honest aside, for the case this guide is not about: if what you actually want is to take over a dropped domain's ranking, a redirect was always the fragile way to do it, and Google just made it fragiler. The durable version is a real site on that domain, standing on its own. That is a different project from this one, which is about domains that already belong to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is less tidy than it looks
&lt;/h2&gt;

&lt;p&gt;A domain's past comes with it. If the name you own spent an earlier life inside a link scheme, a &lt;code&gt;301&lt;/code&gt; carries that history into your site too, and a clean consolidation can still inherit a dirty reputation — worth reading the backlink profile of an acquired domain before you wire it up.&lt;/p&gt;

&lt;p&gt;Relevance is not binary either. A domain only loosely related to your site sits in the grey zone where Google may pass some of the signal, all of it, or none, and no dashboard tells you which ahead of time. The safe reading is that an unrelated &lt;code&gt;301&lt;/code&gt; is a waste rather than a punishment: the equity you hoped to move never arrives, while the redirect sits there doing nothing for anyone. The Google policy wording here was checked on 25 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one setup does not give you
&lt;/h2&gt;

&lt;p&gt;For a handful of domains you own, the whole job is the runbook and one Bulk Redirects list, pointed at the right pages and checked once. That is genuinely all of it, and you should start there.&lt;/p&gt;

&lt;p&gt;At the scale where a portfolio is dozens of names funnelling into one site, the work is staying on the right side of the line continuously: every redirect relevant, every one a single hop to a live page, and none quietly turned into a chain or a homepage dump as the site changes underneath them — plus a re-check each time Google tightens the screws. That standing watch across the whole portfolio is the part &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; runs for you. For a few owned domains, a Bulk Redirects list and the runbook are enough.&lt;/p&gt;

</description>
      <category>redirects</category>
      <category>bulkredirects</category>
      <category>cloudflare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Auto-renew was on and the domain expired anyway</title>
      <dc:creator>301ST</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:00:17 +0000</pubDate>
      <link>https://dev.to/301st/auto-renew-was-on-and-the-domain-expired-anyway-1kch</link>
      <guid>https://dev.to/301st/auto-renew-was-on-and-the-domain-expired-anyway-1kch</guid>
      <description>&lt;p&gt;The domain is on auto-renew. You set it years ago and stopped thinking about it, which was the whole point. Then one morning it returns &lt;code&gt;NXDOMAIN&lt;/code&gt;, and everything riding on it is gone at once: the site, the email on that domain, and &lt;a href="https://301.sh/every-way-to-redirect-on-cloudflare/" rel="noopener noreferrer"&gt;every redirect&lt;/a&gt; pointed through it. Auto-renew was the one thing that was supposed to make this impossible.&lt;/p&gt;

&lt;p&gt;It isn't, and it fails people who register domains for a living, not only the careless. The setting sits there looking healthy while the three things it quietly depends on rot out of your sight. Knowing where the net has holes — and what actually catches a domain falling through them — is the difference between finding out on a schedule and finding out from a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-renew is a standing instruction, not a guarantee
&lt;/h2&gt;

&lt;p&gt;Auto-renew is one sentence: charge this card, file this renewal, on this date. It inherits every way those two things quietly rot.&lt;/p&gt;

&lt;p&gt;The card expires or is declined. Auto-renew is a card on file for a domain you registered in 2019, and a card from 2019 has almost certainly been reissued since. The instruction is intact; the payment behind it is not.&lt;/p&gt;

&lt;p&gt;The notification you were counting on never arrives. It goes to an address you no longer read, or a filter files it under a tab you never open, or it is simply misrouted to spam. The single message that would tell you the charge failed is the message you don't see, and its absence looks exactly like everything being fine.&lt;/p&gt;

&lt;p&gt;And you have stopped looking. Renewals you automate are renewals you take off your desk — that is the appeal and also the failure. You might have renewed the domain's TLS certificate in the spring and half-filed the registration under the same mental heading, so the date never stood out. The domain drops out of your attention on the day you trust a machine to hold it, and when the machine misses there is no second reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cloudflare Registrar actually does
&lt;/h2&gt;

&lt;p&gt;Cloudflare Registrar is better than most on the mechanics, and it is worth knowing the real schedule instead of assuming a single make-or-break charge. From its documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloudflare Registrar enrolls your domain to auto-renew by default.&lt;/p&gt;

&lt;p&gt;Cloudflare attempts to renew these domains automatically 30 days before their expiration date. Several more attempts are made if the first attempt fails. The last attempt to renew is made on the day before expiration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a month of retries, not one roll of the dice. And if every attempt in that window fails:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you do not renew your domain before the expiration date, your domain will enter a Redemption Grace Period (RGP) for 30 days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a Cloudflare-registered name that lapses is not gone the next morning. It can be restored during redemption. For domains actually on Cloudflare Registrar this is a genuine net, and it costs nothing — checked against Cloudflare's documentation on 25 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the net has holes
&lt;/h2&gt;

&lt;p&gt;The schedule is good. The things it still depends on are the same three that rot out of sight.&lt;/p&gt;

&lt;p&gt;It still needs a card that works. Thirty days of retries against a declined card end where a single failed charge does — on the day before expiration — and then you are counting redemption days instead of holding a domain. The retries buy time only if someone notices them.&lt;/p&gt;

&lt;p&gt;It only covers domains registered with Cloudflare. A portfolio is rarely all in one place. Names accrete over years across three or four registrars, each with its own renewal timing and its own notification habits, most of them worse than Cloudflare's. The one you forgot you owned is the one no auto-renew setting is watching.&lt;/p&gt;

&lt;p&gt;And renewed is not the same question as resolving. &lt;code&gt;NXDOMAIN&lt;/code&gt; is what a lapsed registration returns, but it is also what a zone hold, a nameserver that drifted to &lt;a href="https://301.sh/redirect-silently-fails/" rel="noopener noreferrer"&gt;an account nobody can find&lt;/a&gt;, or a never-activated zone returns. "Did it renew?" is answered in a billing page you have to remember to open. "Is it resolving?" is answered only by asking the domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually catches it
&lt;/h2&gt;

&lt;p&gt;The lesson is not "check your card." It is that every signal pointing inward — the charge, the email, your own memory — can fail quietly and at the same time. The only signal that can't is the one pointing at the result: does the name still resolve, and did that answer change since yesterday.&lt;/p&gt;

&lt;p&gt;Line up the failures against the things that might catch each one, and only one column is filled all the way down:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What fails&lt;/th&gt;
&lt;th&gt;Auto-renew&lt;/th&gt;
&lt;th&gt;Registrar email&lt;/th&gt;
&lt;th&gt;A reminder you keep&lt;/th&gt;
&lt;th&gt;Probing the domain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Card declined&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;if you read it&lt;/td&gt;
&lt;td&gt;the date, not the failure&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification misrouted or ignored&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain at a registrar you forgot&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renewed, still &lt;code&gt;NXDOMAIN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the one no renewal-tracking method can catch by definition: the registration is fine and the domain still does not answer. A spreadsheet with &lt;code&gt;=DATEDIF()&lt;/code&gt; counting down to a renewal date — the advice that has not changed in ten years — tracks the date you typed in, not the domain. If the name lapses early because a charge failed, or the date in the sheet was wrong, the sheet stays green while the domain is dead.&lt;/p&gt;

&lt;p&gt;Probing the domain is the same loop as the &lt;a href="https://301.sh/audit-300-domains-one-script/" rel="noopener noreferrer"&gt;audit script&lt;/a&gt;, with the one column that matters here: whether a name that resolved last time returns &lt;code&gt;NXDOMAIN&lt;/code&gt; now. Run it on a schedule, compare each answer to the last, and a name that quietly lapsed raises a flag while it is still inside the redemption window, instead of after a customer emails to say the site is down.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is worth setting up, and when it isn't
&lt;/h2&gt;

&lt;p&gt;For domains on Cloudflare Registrar, the free path is genuinely good: auto-renew by default, a month of retries, and a redemption window if all of them miss. Turn it on, keep a working card, and for a handful of names that is the whole job — a calendar reminder you control, kept somewhere other than the registrar's own email, covers the rest.&lt;/p&gt;

&lt;p&gt;Where it runs out is a portfolio spread across registrars, where no single renewal setting sees all of it and the only thing that does is a probe asking every domain, on every registrar, whether it is still there — and comparing today's answer to yesterday's. That cross-registrar watch, on a loop, with an alert when a name changes, is the part &lt;a href="https://301.st" rel="noopener noreferrer"&gt;301.st&lt;/a&gt; runs for you. Auto-renew keeps the domains it owns; this keeps track of the ones nobody remembered to look at. For a dozen names at one registrar, auto-renew and a reminder on your own calendar are genuinely enough.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>cloudflare</category>
      <category>redirects</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
