<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ipv6 on bismigel</title><link>https://bismigel.dev/tags/ipv6/</link><description>Recent content in Ipv6 on bismigel</description><generator>Hugo</generator><language>en-us</language><copyright>bismigel</copyright><lastBuildDate>Thu, 22 Oct 2026 22:15:00 -0400</lastBuildDate><atom:link href="https://bismigel.dev/tags/ipv6/index.xml" rel="self" type="application/rss+xml"/><item><title>IPv6 packet loss, not slow DNS: why Jellyfin buffered off the LAN</title><link>https://bismigel.dev/posts/2026-10-22-ipv6-loss-not-slow-dns/</link><pubDate>Thu, 22 Oct 2026 22:15:00 -0400</pubDate><guid>https://bismigel.dev/posts/2026-10-22-ipv6-loss-not-slow-dns/</guid><description/><content:encoded><![CDATA[<p>Buffering from outside the LAN is the kind of report that invites guessing. The usual suspects are the transcode, the disk, or the network. This time the report came with a detail that did not fit any of them: the site felt slow &ldquo;only at DNS&rdquo;, and from a machine on <code>192.168.2.0/24</code> everything was fast.</p>
<p>That detail was the clue. DNS was not slow. One address family was losing packets, and everything else followed from that.</p>
<h1 id="the-report">The report</h1>
<p>The user was streaming from a remote network. Playback started, stalled after a few seconds, restarted, and stalled again. From inside the LAN the same file played normally.</p>
<p>The first instinct is to blame the server. The server was idle:</p>
<ul>
<li>node CPU 41% of 4 cores, load 2.56, iowait 3.7%;</li>
<li>disk utilisation 1.9% on NVMe and 1.45% on the media disk;</li>
<li>Jellyfin using 0.51 cores and 783 MiB of a 4 GiB limit;</li>
<li>about 1.8 Mbit/s on the wire, which is exactly one stream.</li>
</ul>
<p>Nothing on the box was the constraint. The Jellyfin log showed the real shape of the failure: ffmpeg started and was stopped a dozen times in six minutes, and every stop was clean (<code>FFmpeg exited with code 0</code>). The server was not crashing. The client was giving up and asking again.</p>
<h1 id="measure-both-families">Measure both families</h1>
<p>The remote client was dual-stack. I asked for the same request over each family on that machine:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -4 -sk -o /dev/null -w <span class="s2">&#34;v4 dns=%{time_namelookup} tcp=%{time_connect} ttfb=%{time_starttransfer}\n&#34;</span> https://jellyfin.bismigel.dev/System/Info/Public
</span></span><span class="line"><span class="cl">curl -6 -sk -o /dev/null -w <span class="s2">&#34;v6 dns=%{time_namelookup} tcp=%{time_connect} ttfb=%{time_starttransfer}\n&#34;</span> https://jellyfin.bismigel.dev/System/Info/Public</span></span></code></pre></div>
</div>
<p>One pair said IPv6 was slow. That was not the whole story, so we ran it five more times:</p>
<table>
	<thead>
			<tr>
					<th>run</th>
					<th>v4 dns</th>
					<th>v4 tcp</th>
					<th>v4 ttfb</th>
					<th>v6 dns</th>
					<th>v6 tcp</th>
					<th>v6 ttfb</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>0.013</td>
					<td>0.023</td>
					<td>0.094</td>
					<td>0.015</td>
					<td>1.044</td>
					<td>1.246</td>
			</tr>
			<tr>
					<td>2</td>
					<td>0.015</td>
					<td>0.025</td>
					<td>0.185</td>
					<td>0.015</td>
					<td>0.025</td>
					<td>0.240</td>
			</tr>
			<tr>
					<td>3</td>
					<td>0.016</td>
					<td>0.026</td>
					<td>0.096</td>
					<td>0.012</td>
					<td>0.022</td>
					<td>0.169</td>
			</tr>
			<tr>
					<td>4</td>
					<td>0.012</td>
					<td>0.022</td>
					<td>0.134</td>
					<td>0.011</td>
					<td>0.021</td>
					<td>0.302</td>
			</tr>
			<tr>
					<td>5</td>
					<td>0.012</td>
					<td>0.023</td>
					<td>0.078</td>
					<td>5.016</td>
					<td>5.027</td>
					<td>5.215</td>
			</tr>
			<tr>
					<td>6</td>
					<td>0.012</td>
					<td>0.022</td>
					<td>0.151</td>
					<td>0.011</td>
					<td>0.022</td>
					<td>0.192</td>
			</tr>
	</tbody>
</table>
<p>Look at runs 2, 3, 4 and 6: IPv6 is as fast as IPv4. Twenty milliseconds to connect, both families. Then run 1 and run 5 are one and five seconds.</p>
<p>That is not a slow path. That is loss with retransmission:</p>
<ul>
<li><code>tcp=1.044</code> is a lost SYN, retried on the kernel backoff;</li>
<li><code>dns=5.016</code> is a DNS query that was sent, dropped, and retried on the resolver&rsquo;s five second timer. Nothing had been sent to Cloudflare yet.</li>
</ul>
<p>My first framing was wrong. I called it a constant 45x penalty after one run. Six runs turned it into intermittent loss, which is a different failure and a worse one for video, because it breaks in bursts instead of being uniformly slow.</p>
<h1 id="why-it-looked-like-slow-dns">Why it looked like slow DNS</h1>
<p>Every hostname in the zone is a proxied CNAME to the tunnel. With Cloudflare IPv6 compatibility on, the zone answers both families for all of them:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">A     172.64.80.1                              TTL 282
</span></span><span class="line"><span class="cl">AAAA  2606:4700:130:436c:6f75:6466:6c61:7265   TTL 282</span></span></code></pre></div>
</div>
<p>A dual-stack client will prefer AAAA. Browsers handle a bad IPv6 path with Happy Eyeballs: they race both families and drop the loser after about 250 ms. Most native apps do not. Jellyfin&rsquo;s web client is a browser, but its TV and mobile clients are not, and neither are the small tools people test with.</p>
<p>So the client sat in the timeout, the user saw a spinner, and the natural report is &ldquo;DNS is slow&rdquo;. For one second or five seconds at a time, that is exactly what it was.</p>
<h1 id="why-loss-becomes-buffering">Why loss becomes buffering</h1>
<p>TCP throughput falls roughly as <code>1 / (RTT * sqrt(loss))</code>. One percent loss is not a small tax; it is close to an order of magnitude on a long path.</p>
<p>Jellyfin&rsquo;s HLS player measures the throughput it gets and picks a bitrate from it. On the lossy path it measured about 1 Mbit/s and locked into a 720p transcode:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">02:11:28  -b:v 1116000  ... scale_vaapi=w=1280:h=720 ...</span></span></code></pre></div>
</div>
<p>Every stall also killed the HLS session. The next request started a new ffmpeg job, which is the restart storm in the log. The server was doing exactly what it was told; the client could not pull the segments fast enough to keep the session alive.</p>
<h1 id="the-shape-of-the-path">The shape of the path</h1>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">dual-stack client
</span></span><span class="line"><span class="cl">      |
</span></span><span class="line"><span class="cl">      |  1. DNS: A and AAAA, both answered (proxied CNAME to the tunnel)
</span></span><span class="line"><span class="cl">      v
</span></span><span class="line"><span class="cl">Cloudflare anycast     172.64.80.1        /  2606:4700:130:436c:...
</span></span><span class="line"><span class="cl">      |                      |                        |
</span></span><span class="line"><span class="cl">      |  2. client prefers AAAA  -&gt;  loss here -&gt; 1 s connect, 5 s DNS retry
</span></span><span class="line"><span class="cl">      |                      |                        |
</span></span><span class="line"><span class="cl">      |  3. tunnel connector (cloudflared on .81 and .82, IPv4/QUIC)
</span></span><span class="line"><span class="cl">      v
</span></span><span class="line"><span class="cl">ingress-nginx :32080  -&gt;  Jellyfin pod on .81  -&gt;  /mnt/media</span></span></code></pre></div>
</div>
<p>The tunnel is not part of the problem. The connector talks to Cloudflare over IPv4 either way. What changes is which family the client uses to reach the edge.</p>
<h1 id="the-fix-at-the-edge-not-on-the-client">The fix at the edge, not on the client</h1>
<p>There is no per-hostname switch for this. While IPv6 compatibility is on, Cloudflare serves AAAA for every proxied record in the zone, and a proxied record cannot be made A-only. The options were:</p>
<ol>
<li>fix every client network that has bad IPv6, forever;</li>
<li>turn the zone&rsquo;s IPv6 off, once.</li>
</ol>
<p>For this lab the second one costs nothing. Everything public is a Cloudflare anycast front door in front of a tunnel. No service here needs IPv6, and any client that worked was already falling back to IPv4.</p>
<p>The OpenTofu change is four lines, next to the other zone settings:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">hcl</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;cloudflare_zone_setting&#34; &#34;ipv6&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  zone_id</span>    <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">zone_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  setting_id</span> <span class="o">=</span> <span class="s2">&#34;ipv6&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  value</span>      <span class="o">=</span> <span class="s2">&#34;off&#34;</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>That is PR <code>bakuran-infra</code> #69 (private repository). The change went in through <code>tofu apply</code>, not through the dashboard, so the reason lives in the repo next to the setting.</p>
<p>One thing the documentation does not say clearly: Cloudflare&rsquo;s docs present turning IPv6 compatibility off as an Enterprise option, and suggest Pseudo IPv4 for everyone else. On this account the API accepted <code>ipv6 = off</code> and the AAAA answers stopped. I verified the result instead of trusting the plan:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -H <span class="s1">&#39;accept: application/dns-json&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s1">&#39;https://cloudflare-dns.com/dns-query?name=jellyfin.bismigel.dev&amp;type=AAAA&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># returns no Answer section after the change</span></span></span></code></pre></div>
</div>
<h1 id="what-it-bought">What it bought</h1>
<p>The client had an AAAA cached for the TTL (282 seconds) and an open HTTP/2 connection, so nothing changed at once. When the page was reloaded, the session re-authenticated and the source address changed from <code>2a02:6ea0:...</code> to <code>185.98.171.223</code>. Two things happened in the same minute:</p>
<table>
	<thead>
			<tr>
					<th>metric</th>
					<th>before</th>
					<th>after</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>source family</td>
					<td>IPv6</td>
					<td>IPv4</td>
			</tr>
			<tr>
					<td>target bitrate</td>
					<td>1116000</td>
					<td>9753335</td>
			</tr>
			<tr>
					<td>downscale</td>
					<td>720p</td>
					<td>none</td>
			</tr>
			<tr>
					<td>transcoder restarts</td>
					<td>every 10 to 30 seconds</td>
					<td>one per user seek</td>
			</tr>
	</tbody>
</table>
<p>The bitrate went up 8.7x, and the <code>w=1280:h=720</code> downscale disappeared from the ffmpeg command. The stream was not only smoother, it was higher quality. The last restart in the log was a seek to 00:18:46, which is a user action, not a failure.</p>
<p>That is the part I did not expect. I thought removing the lossy path would stop the buffering. It also let the player re-measure the link and take 9.75 Mbit/s instead of 1.1 Mbit/s, because the loss was what made it choose the low ladder in the first place.</p>
<h1 id="the-second-limit-underneath-it">The second limit underneath it</h1>
<p>With the path fixed, the next test was a 4K file: <code>Hush (2016)</code>, 2160p HDR HEVC. This time the ffmpeg line said something else:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">02:20:03  -b:v 11616000  scale_vaapi=w=2560:h=1072
</span></span><span class="line"><span class="cl">          procamp_vaapi, tonemap_vaapi=format=nv12:p=bt709:t=bt709:m=bt709
</span></span><span class="line"><span class="cl">02:20:05  MediaInfoHelper: RemoteClientBitrateLimit: 12000000 ...</span></span></code></pre></div>
</div>
<p>The stream was being tone-mapped from HDR to SDR and re-encoded at 11.6 Mbit/s, not because of the network this time but because of a server default: the Internet streaming bitrate limit of 12 Mbit/s. It applies to every client Jellyfin considers remote, and it caps direct play as well as transcoding. The client could ask for 40 Mbit/s and still get 11.6.</p>
<p>That value is not a Kubernetes field. It lives in <code>config/system.xml</code> on the <code>jellyfin-config</code> PVC, which is why nobody sees it in the repo. It is removed by a one-shot Job that calls the API and verifies the result: <code>bakuran-gitops</code> PR #151.</p>
<p>After it ran, the same file started at 31.7 Mbit/s, still tone-mapping but with no downscale:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">02:37:06  -b:v 31673882  procamp_vaapi, tonemap_vaapi ...</span></span></code></pre></div>
</div>
<p>Two lessons from that: a transport fix can expose a policy limit underneath it, and a setting that lives on a PVC is invisible until it bites.</p>
<h1 id="what-actually-limits-it-now">What actually limits it now</h1>
<p>With both caps gone, the honest ceiling list for this lab, measured rather than assumed:</p>
<table>
	<thead>
			<tr>
					<th>limit</th>
					<th>measured value</th>
					<th>what it means</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>node NIC</td>
					<td>1 Gbps each (<code>node_network_speed_bytes</code> 125000000)</td>
					<td>~940 Mbit/s per node, about 23 streams at 40 Mbit/s</td>
			</tr>
			<tr>
					<td>WAN upload</td>
					<td>3 Gbps</td>
					<td>wider than the NIC, so never the binding constraint</td>
			</tr>
			<tr>
					<td>CPU, 4K HDR tone-map at 31.7 Mbit/s</td>
					<td>0.80 cores per stream</td>
					<td>~4 concurrent before 4 cores saturate</td>
			</tr>
			<tr>
					<td>CPU, same at 11.6 Mbit/s downscaled</td>
					<td>0.58 cores per stream</td>
					<td>the cheaper shape before the cap was removed</td>
			</tr>
			<tr>
					<td>CPU, direct play</td>
					<td>near zero (<code>codec:v copy</code>)</td>
					<td>remux only, limit becomes disk and NIC</td>
			</tr>
	</tbody>
</table>
<p>The order matters. Direct play is cheap and the NIC decides. A 4K HDR title that needs tone-mapping is expensive and the CPU decides, long before any cable does. Four people watching the same HDR film now costs 3.2 of the 4 cores, so the fifth one queues. At the old 11.6 Mbit/s target the same four cost 2.3.</p>
<p>That is the real trade in raising the cap: better picture per stream, fewer streams before the box is full.</p>
<h1 id="what-i-would-do-differently">What I would do differently</h1>
<ul>
<li>Measure both address families before theorising. One pair of numbers looked like a constant penalty; six runs showed loss, which is a different diagnosis and a different fix.</li>
<li>Treat a 5 second DNS time as a dropped query, not a slow resolver. The number is a timer, not a measurement of distance.</li>
<li>Remember that the server can be idle while the user is unhappy. CPU, disk and memory were all clean during the worst of it.</li>
<li>Check the cache before declaring a fix live. The client kept using IPv6 for minutes after the zone stopped publishing AAAA, because both the answer and the connection were already open.</li>
<li>Keep going one layer down. Fixing the path made the transcodes cheap, which is what made the 12 Mbit/s policy cap visible in the first place.</li>
<li>Measure again after raising a cap. The CPU per stream went from 0.58 to 0.80 cores when the same 4K file stopped being downscaled, and that changes how many viewers fit.</li>
</ul>
<h1 id="links">Links</h1>
<ul>
<li>The edge change: <code>bakuran-infra</code> PR #69 (private repository)</li>
<li>The bitrate cap removal: <code>bakuran-gitops</code> PR #151 (private repository)</li>
<li>Cloudflare IPv6 compatibility: <a href="https://developers.cloudflare.com/network/ipv6-compatibility/" target="_blank" rel="noopener noreferrer">https://developers.cloudflare.com/network/ipv6-compatibility/</a>
</li>
<li>Cloudflare zone settings API: <a href="https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/edit/" target="_blank" rel="noopener noreferrer">https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/edit/</a>
</li>
<li>Happy Eyeballs, RFC 8305: <a href="https://www.rfc-editor.org/rfc/rfc8305" target="_blank" rel="noopener noreferrer">https://www.rfc-editor.org/rfc/rfc8305</a>
</li>
<li>The OpenTofu migration that made this a four line change: <a href="https://bismigel.dev/posts/2026-10-20-cloudflare-opentofu-migration/">https://bismigel.dev/posts/2026-10-20-cloudflare-opentofu-migration/</a>
</li>
</ul>
]]></content:encoded></item></channel></rss>