diff --git a/docs/troubleshooting/index.md b/docs/troubleshooting/index.md
index 2933ee5..9ae5aa6 100644
--- a/docs/troubleshooting/index.md
+++ b/docs/troubleshooting/index.md
@@ -92,5 +92,6 @@ or just "now I know to look here first."
## Entries
- [Browser Keeps Prompting to Log In on Every Article (polyfill.io)](polyfill-io-login-prompt.md)
+- [Intermittent "CORS Error" / "Failed to Fetch" That's Actually a Stale Browser Cache](stale-browser-cache-fetch-cors-error.md)
- [WiFi Bufferbloat Collapses the Connection When Streaming to a LAN Device (Expo, Steam Link)](wifi-bufferbloat-lan-streaming-drops.md)
- [WiFi Card Drops Connection Under Load Due to PCIe Power Management (mt7925e Forced Reassociation)](wifi-card-pcie-power-reassociation-drops.md)
diff --git a/docs/troubleshooting/stale-browser-cache-fetch-cors-error.md b/docs/troubleshooting/stale-browser-cache-fetch-cors-error.md
new file mode 100644
index 0000000..4d03c60
--- /dev/null
+++ b/docs/troubleshooting/stale-browser-cache-fetch-cors-error.md
@@ -0,0 +1,144 @@
+---
+tags:
+
+- troubleshooting
+- javascript
+- networking
+- browser
+- caching
+
+---
+
+# Intermittent "CORS Error" / "Failed to Fetch" That's Actually a Stale Browser Cache
+
+An intermittent `TypeError: Failed to fetch` with a "blocked by CORS policy" console
+message turned out to have nothing to do with CORS, the server, or the network — the
+browser's HTTP cache was replaying a stale error response for every request to that exact
+URL, until the entry expired.
+
+## Environment
+
+- Symptom: `fetch()` HEAD probe to a cross-origin S3 URL, from a read-aloud audio player
+- Browsers: desktop Chrome, incognito, mobile Samsung Internet — all reproduced it
+- Deployment target: static export (GitHub Pages), not a live dev/prod server
+
+## Problem
+
+- A `fetch()` HEAD probe to a cross-origin URL intermittently failed with
+ `TypeError: Failed to fetch`, and the console showed:
+
+ ```text
+ Access to fetch at '...' has been blocked by CORS policy: No
+ 'Access-Control-Allow-Origin' header is present on the requested resource.
+ ```
+
+- Worked once, then failed reliably for 10-20 minutes, then worked again — reproducible
+ across desktop Chrome, incognito, and mobile Samsung Internet.
+- `curl` from multiple machines/networks, with matching headers, HTTP/2, concurrent
+ bursts — never reproduced it, not once across 100+ requests.
+
+The curl-vs-browser split was the first real clue: whatever was wrong lived somewhere
+`curl` doesn't have — a warm browser cache.
+
+## Root Cause
+
+**The browser's HTTP cache stores an error response (e.g. a 503) for a `fetch()` call and
+keeps replaying it** for every subsequent request to that exact URL+method under default
+cache mode — for minutes, until the entry is evicted. `curl` never hits this because it
+never has a warm browser cache to poison in the first place.
+
+A "blocked by CORS policy: no Access-Control-Allow-Origin header" message is **not proof
+of a CORS misconfiguration** — it's what the browser shows for *any* cross-origin failure
+where the response (real or cached) lacks CORS headers, including a stale cached error.
+
+```mermaid
+sequenceDiagram
+ participant App as App fetch()
+ participant Cache as Browser HTTP Cache
+ participant Server as Origin Server
+
+ App->>Server: fetch(url) — first request
+ Server-->>App: 503 (transient)
+ Note over Cache: Error response cached
under default cache mode
+ App->>Cache: fetch(url) — retry, minutes later
+ Cache-->>App: Replays stale 503 (no CORS headers)
+ Note over App: Browser reports it as a
CORS error, not a cache hit
+ App->>Server: fetch(url, {cache: "no-store"})
+ Server-->>App: 200 (server was fine all along)
+```
+
+### How it was proved (the reusable part)
+
+1. Caught the app's probe failing live via DevTools network inspection.
+1. **In the same tab, at the same moment**, ran a manual `fetch()` to the *exact same URL*
+ from the console:
+ - Same options as the app's fetch (default cache mode) → failed, every time.
+ - Same call but with `cache: "no-store"` added → succeeded, every time.
+1. Same URL, same tab, same instant — the only variable was cache mode. That's about as
+ close to a controlled experiment as you get in a live browser bug.
+1. Independently confirmed via Chrome DevTools' **"Disable cache"** checkbox (Network
+ tab) — failures stopped completely while checked.
+
+## Fix
+
+Add `cache: "no-store"` to any `fetch()` that's a **status/availability probe** —
+something that must always reflect current server state (HEAD checks, health checks,
+"does this resource exist yet" checks, short-lived metadata):
+
+```javascript
+const res = await fetch(url, { method: "HEAD", cache: "no-store" });
+```
+
+Cache the *content* (via normal `Cache-Control` headers on the actual resource), never
+cache the *probe*.
+
+## Prevention
+
+```mermaid
+flowchart TD
+ A[Intermittent fetch failure,
CORS-looking console error] --> B{curl reproduces it?}
+ B -->|Yes, consistently| C[Real server/CORS/network issue]
+ B -->|No, never| D[Toggle DevTools 'Disable cache']
+ D -->|Failures stop| E[Stale cached error response]
+ D -->|Still fails| F[Look elsewhere: proxy, extension, real CORS]
+ E --> G["Add cache: 'no-store' to the probe fetch"]
+```
+
+- **If `curl`/server-side tools can't reproduce a client-reported network failure no
+ matter how hard you try, stop trying to reproduce it server-side** — the bug is
+ client-side.
+- Verify the server's actual live headers with `curl -I` before touching CORS config —
+ don't trust a "CORS" console message at face value.
+- Toggle DevTools "Disable cache" before writing any code — it's a 2-second test for "is
+ this a caching problem."
+- A/B test the *exact* failing request, in the *exact* failing session, changing one
+ variable at a time (cache mode, headers, protocol) via the page's own console — this
+ beats theorizing every time.
+- Don't declare victory on "no console errors" from a single local test — verify against
+ the real target environment, and re-run the *original* repro steps, not just a proxy
+ check.
+- Add `cache: "no-store"` by default to any fetch that's a status/availability probe, not
+ just after you've already been burned by this once.
+
+## Compounding Bugs That Made This Harder to Diagnose
+
+Several separate, real bugs were compounding into similar-looking symptoms during the
+same investigation, which made it easy to mistake a partial fix for *the* fix:
+
+- **A genuine Next.js 14.1.x bug**: under a non-empty `basePath` + static export, the App
+ Router's client-side RSC/flight-payload prefetch computed a malformed URL for the root
+ route, 404ing on every page load and cascading into a full fallback page reload. Fixed
+ by upgrading to 14.2.x.
+- **`trailingSlash: true` changed the symptom without fixing the bug** — the malformed
+ URL changed shape but was still wrong. Don't assume a config change "fixed" something
+ just because the exact error string changed; re-verify against the *original* repro.
+- **A missing retry path**: the component's own probe had a "probe once, ever" guard —
+ if the first probe failed for any reason (including the cache issue above), it was
+ stuck showing the error forever with no recovery short of a full remount. Worth fixing
+ independently, but retrying alone would've just kept re-hitting the same poisoned cache
+ entry.
+- **A misleading local test environment**: `next dev`/`next start` (a live server) can't
+ reproduce bugs specific to a real static-export deployment (no dynamic RSC endpoint, no
+ image optimization endpoint). A Docker Compose + nginx replica of the real deployment
+ shape catches that class of bug — though even that replica can't catch the cache bug,
+ since browser HTTP caching is a client-side concern no server replica reproduces.