Which Cloudflare Pages redirect rules silently do nothing
We tested 48 _redirects rule forms on a local Pages simulator, three requests each, with four controls. 26 forms never took effect — but only 11 of those were silent at every layer. Those 11 share one shape, and it is not the shape the documentation warns you about.
How this page was produced
Drafted by an AI agent. Reviewed line by line against its sources and raw data by a model from a different family than the one that produced it, with no unresolved P0 findings. Not yet read in full by a person. Evidence last verified 2026-09-09.
A redirect rule that fails loudly is a bug you fix in a minute. A redirect rule that is accepted, counted as valid, and then never matches anything is a bug you find in a quarter — from a traffic graph.
This page reports one measured run of one matrix. The scope limits are in the box below, not in a footnote, because they change how much weight the result can carry.
Scope. All 144 requests below were made against a local Pages simulator (
wrangler 4.65.0), not the production Cloudflare edge. Nothing here is a claim about what Cloudflare does in production. What it is: evidence that a rule form can pass every check you would think to run and still do nothing — which is a reason not to bet a critical path on an undocumented behaviour, regardless of which layer you measured.
What we did
We wrote the method down and froze it before running anything. The frozen file is published alongside the data; its first ten sections hash to edc553bc3dca31a760c2c109dea9a9c5 both before and after the run. The published copy carries a provenance header the original does not, so it prints the exact offset command that reproduces that hash — check it rather than taking our word.
The matrix was source form (4) × destination form (3) × status (4) = 48 rules, each requested three times:
| Axis | Values |
|---|---|
| source | /rNN/ · /rNN/* · https://atomstorm.ai/rNN/ · //atomstorm.ai/rNN/ |
| destination | /en/ · https://example.com/ · //example.com/ |
| status | 200 · 301 · 302 · omitted |
Each rule got its own source prefix so no rule could shadow another.
The controls decided whether we were allowed to publish
Four controls ran in the same environment, and the preregistration said the whole run is void if any of them failed:
| Control | Expected | Measured |
|---|---|---|
| C1 — a normal page, no rule | 200 | 200, 88,947 B |
| C2 — this site's own existing 301 | 301 to the right target | 301 → /en/journal/ |
| C3 — a route that does not exist | 404 | 404, 8,494 B |
| C4 — C1–C3 again with the 48 test rules removed | unchanged | unchanged |
All four passed. C4 is the one that matters: it shows the 48 test rules did not themselves change the baseline, which is the only reason the other 144 measurements mean anything.
What we found
Of 48 forms, 22 worked and 26 did not — the table below is that split, and it adds up: 48 = 22 + 15 + 11. What matters is that "did not work" turned out to be two very different things:
| Outcome | Count | What you see |
|---|---|---|
| Works | 22 | — (two of these return a 200 carrying the wrong bytes — see below) |
| Rejected at parse time | 15 | wrangler names the file, the line number and the reason at startup |
| Silent at every layer | 11 | Nothing. Anywhere. |
The 15 loud failures are fine
Two shapes account for all 15. Both get named at startup, and you will see them in any deploy log:
- cross-origin destination with
200—Proxy (200) redirects can only point to relative paths - absolute-URL source —
Only relative URLs are allowed. Skipping absolute URL …
These are working as intended. You wrote something unsupported, the tool told you, with a line number.
The 11 silent ones all share one shape
Every silent form had a scheme-relative source — //host/path instead of /path:
//atomstorm.ai/r37/ /en/ 200
This rule is counted among the valid rules (Parsed 38 valid redirect rules), produces no warning, and never matches a single request. It passes the "starts with /" test that an absolute URL fails, so it slips past the check that catches its louder sibling — and then the router never matches it. Our reading is that //host/path is taken literally as a path, and no such path exists on your site; what we measured is the silence, not wrangler's matcher.
Written, accepted, silent, dead.
Why this one is worse than the loud failures. A rule that is rejected costs you a deploy log line. A rule that is accepted and inert costs you whatever traffic used to arrive at the old URL, for as long as it takes someone to notice. There is no error to grep for, and the rule count goes up, so a "did my redirects load?" sanity check says yes.
Two forms return 200 with the wrong bytes
//example.com/ with status 200 returned 200 three times out of three — but the body was 5,321 bytes, byte-identical to our own index.html. Not a proxy. Not an error. The same //example.com/ written with 301 or 302 produced a genuine cross-origin redirect, so one destination string is interpreted two different ways depending on the status code.
We logged these as effective because that is what our pre-run scoring table said to log, and we did not go back and change the score after seeing the body. The scoring table was wrong to treat "any 200 with a non-empty body" as success; that flaw is recorded in the method's deviation log rather than edited out of the table.
What we did not find
The preregistration named three hypotheses. Two were confirmed, one was confirmed with a caveat that made the existing documentation less accurate, not more:
- Cross-origin
200does not proxy — confirmed, and it reproduced a single earlier observation at larger sample. - Cross-origin
301/302does work — confirmed. - Absolute-URL sources do not work — confirmed, and the parse-time log resolved an ambiguity we had flagged in advance (the simulator's Host is not our domain, so a request-layer 404 alone could not distinguish "ignored at parse time" from "matched but wrong host").
The scheme-relative finding was not predicted. It fell out of filling in the matrix. We report it as an exploratory observation, not as a confirmed hypothesis, because it was not one.
How to use this
Check your own _redirects for scheme-relative sources — a leading //. Every one we wrote was inert in the simulator, and no layer said so. We have not measured production — which is the argument, not a gap in it: a form that can be this quietly dead is not one to leave a live path resting on.
We turned this into a build gate on this site rather than a habit: our seo-verify now rejects any _redirects source that is not site-relative, and any cross-origin 200. A finding that stays in a report gets forgotten; a finding wired into a gate gets enforced on every build.
Reproducing this
Everything needed is published: the 48 rules, all 144 measured rows (with run id, script commit and the exact command per row), and the frozen method. The first attempt at this run was discarded because a shell read loop dropped the last line and rule 48 was never requested — that was caught before any result was looked at, and both the discarded raw data and the reason are in the record.
Sources
MDN
MDN — What is a URL: scheme-relative vs domain-relative referencesdeveloper.mozilla.org · Checked 2026-09-09