Fixing Redirect Loops on Cloudflare Pages

"This page isn't working — docs.example.com redirected you too many times." A redirect loop on a static site is maddening, because nothing in the site's code redirects anything: the loop lives in the interaction between redirect rules in different places. On Cloudflare Pages there are at least four such places — the project's _redirects file, Pages' own trailing-slash handling, the zone's Page Rules or Redirect Rules, and the zone's SSL setting — and two of them agreeing to disagree produces an infinite loop.

This guide works through three loops on a marketing site and docs on Cloudflare Pages that appeared over a few months, traces each with curl, and fixes the rule causing it. It belongs to Cloudflare Pages Edge Caching Setup; the redirect configuration itself is covered in Configuring Redirects on Cloudflare Pages.

Trace the Loop First

Guessing at redirect rules wastes time. One command shows every hop:

curl -sIL --max-redirs 8 https://example.com/docs/install 2>&1 | grep -iE '^(HTTP|location|server|cf-ray)'
HTTP/2 308
location: /docs/install/
HTTP/2 301
location: https://example.com/docs/install
HTTP/2 308
location: /docs/install/
...
curl: (47) Maximum (8) redirects followed

Two URLs alternating, with two different status codes, means two different rules. The 308 is characteristic of Pages' trailing-slash handling; the 301 came from somewhere else.

Redirect loop between Pages trailing slash handling and a _redirects rule Three actors, the browser, Pages asset handling and the _redirects rules. The browser requests docs install without a slash. Pages finds docs install index.html and redirects with 308 to the URL with a slash. The browser requests the URL with a slash. A _redirects rule strips trailing slashes and redirects with 301 back to the URL without a slash. The cycle repeats until the browser gives up. Loop 1 in a trace Browser Pages assets _redirects GET /docs/install 308 → /docs/install/ GET /docs/install/ 301 → /docs/install …repeats until the browser stops
Pages wants a trailing slash for files named index.html; the old rule wanted none.

Loop 1: _redirects Fighting Pages' Trailing-Slash Handling

The site had moved from Netlify, whose _redirects contained a rule to strip trailing slashes:

/*/  /:splat  301

On Pages, a request for /docs/install finds docs/install/index.html and Pages redirects it to /docs/install/ with a 308 — directory index files get a trailing slash. The old rule then redirected /docs/install/ back to /docs/install. Loop.

Fix: delete the rule and let Pages handle slashes. Pages' behaviour follows file names: about/index.html is served at /about/, about.html at /about. To change which form is canonical, change how the build writes files (Astro's build.format, Hugo's uglyURLs), not the redirects. Links in the site should use the same form, as in Clean URLs and Trailing Slashes on S3, which covers the same choice on another host.

Loop 2: Apex and www Redirected Both Ways

Weeks later, https://example.com looped while https://www.example.com worked. The trace:

HTTP/2 301   location: https://www.example.com/      (server: cloudflare, from a Redirect Rule)
HTTP/2 301   location: https://example.com/          (from _redirects)

A colleague had added a zone-level Redirect Rule sending the apex to www for a marketing campaign. The project's _redirects already had a host-based rule doing the opposite (https://www.example.com/* https://example.com/:splat 301). Zone Redirect Rules run before the request reaches Pages, so both fired, one on each hop.

Fix: choose one canonical host and one place for host redirects. The team kept the apex as canonical and removed the zone Redirect Rule. Host-level redirects now live only in the zone's Redirect Rules (documented in the repository's README) and _redirects contains only path redirects — the split prevents the next person from adding the opposite rule in the other place.

Sources of redirects on a Cloudflare Pages site and what each should handle A matrix of four redirect sources. Zone Redirect Rules run first and should handle host and protocol redirects. The SSL and Always Use HTTPS settings handle HTTP to HTTPS. The project's _redirects file runs on Pages and should handle path redirects only. Pages' own asset handling adds or removes trailing slashes based on file names and should not be fought. Where redirects can come from on Pages Source Runs Should handle Zone Redirect Rules before Pages host: apex ↔ www Always Use HTTPS before Pages http → https _redirects file on Pages old paths → new paths Pages asset handling on Pages trailing slash from file names
Each kind of redirect belongs in exactly one place; loops come from the same kind in two.

Loop 3: Flexible SSL on a Proxied Origin

The third loop appeared only on a subdomain, status.example.com, which was not on Pages but a small VM behind Cloudflare's proxy, linked from the site. The zone's SSL mode was Flexible: Cloudflare connects to the origin over HTTP. The VM's Nginx redirected all HTTP to HTTPS. So: browser → Cloudflare (HTTPS) → origin (HTTP) → 301 to HTTPS → browser → Cloudflare (HTTPS) → origin (HTTP) → 301…

Fix: set SSL to Full (strict) and install a certificate on the origin (a free Cloudflare Origin CA certificate works), so Cloudflare connects over HTTPS and the origin's redirect never fires. Pages projects themselves are not affected by this setting's loop, but a Flexible zone setting will catch any proxied origin on the same zone.

Clearing Cached Loops

After each fix, some readers still reported the loop. Browsers had cached the 301s. The fixes were verified with curl and private windows, and for the duration of future redirect experiments the team uses 302 or 307 until the rule is confirmed, switching to 301 or 308 afterwards. A cached permanent redirect cannot be recalled from a reader's browser.

Loops That Involve Workers or Functions

Sites that add Pages Functions or a Worker in front can create the same pattern in code: middleware that normalises paths (lowercasing, adding slashes) while the asset layer normalises them differently. The rule is the same — one place decides the canonical form. On this site the middleware only handles language redirects, and it checks new URL(request.url).pathname against the canonical form before redirecting, so it never issues a redirect to a URL the asset layer would redirect back.

Reading Which Product Sent a Redirect

The response headers on each hop usually say where it came from. Redirects issued by Pages' asset handling are 308s with no extra headers; zone Redirect Rules and Page Rules return 301 or 302 with server: cloudflare but before any Pages-specific headers; redirects from _redirects carry the project's custom headers if _headers applies to the path; and origin redirects behind a proxied zone include the origin's own server header, such as nginx. Comparing those on two consecutive hops identified the two rules in all three loops within minutes.

Prevent the Next Loop

A CI check runs curl traces against a list of canonical URLs in both host forms, with and without trailing slashes, on every preview deploy, and fails if any chain is longer than two hops or ends in anything but 200:

while read u; do
  hops=$(curl -sIL --max-redirs 5 -o /dev/null -w '%{num_redirects} %{http_code}' "$u")
  case "$hops" in 0\ 200|1\ 200|2\ 200) ;; *) echo "BAD $u -> $hops"; fail=1;; esac
done < redirect-cases.txt; exit ${fail:-0}

Zone-level rules are not part of preview deploys, so the same script also runs against production on a schedule. Testing Redirects and Headers Before Deploy has the fuller version.

Measured Impact

MeasureDuring loopsAfter fixesTool
URLs looping (of 40 tested)40 / 3 / 1 (per incident)0curl trace script
Max redirect hops to a canonical URL∞1curl trace script
"Redirect error" in Search Console1,240 URLs0 after recrawlPage indexing report
Rules in _redirects212186 (host and slash rules removed)File
Redirect hops for common URL variants after the fixes Vertical bars. The canonical URL itself 0 hops. The URL without trailing slash 1 hop. The www host 1 hop. HTTP on the apex 1 hop. HTTP on www 2 hops. Redirect hops to reach a canonical page 0 Canonical 1 No trailing slash 1 www host 1 http apex 2 http www
curl traces from the scheduled production check; every variant reaches a 200 in at most two hops.

Pitfalls & Rollback

  • Carrying trailing-slash rules over from another host. Pages handles slashes from file names.
  • Host redirects in two places. Keep them in zone Redirect Rules only.
  • Flexible SSL with an HTTPS-redirecting origin. Use Full (strict).
  • Testing with permanent redirects. Use 302/307 until confirmed.
  • Rollback: each fix removes or changes one rule; a removed rule can be restored, but trace it with curl before and after.

Conclusion

Redirect loops on Cloudflare Pages come from two rules in different places that disagree: a _redirects slash rule against Pages' own handling, host redirects in both the zone and the project, or Flexible SSL against an origin that redirects to HTTPS. Tracing every hop with curl, giving each kind of redirect one home, and checking redirect chains in CI and on a schedule has kept every URL variant within two hops of its page.

FAQ

Why does my Cloudflare Pages site show too many redirects?

Two or more rules keep sending the browser between URLs. Common pairs on Pages are a _redirects rule that adds or removes a trailing slash fighting Pages' own trailing-slash handling, a redirect from apex to www in Page Rules with the opposite in _redirects, and HTTP to HTTPS redirects combined with Flexible SSL on a proxied origin. Tracing the redirect chain with curl shows which rules are involved.

How do I trace a redirect loop?

Run curl with the location option and a maximum number of redirects, printing each status and location, for example curl -sIL --max-redirs 10 and the URL. The output lists every hop. A loop shows the same two URLs alternating; the headers on each hop often reveal which product issued it.

Does Cloudflare Pages add trailing slashes itself?

Yes. Pages serves about slash index.html at both slash about and slash about slash, and redirects between them with a 308 depending on how the file is named. A _redirects rule that redirects in the opposite direction creates a loop. Let Pages handle trailing slashes and remove such rules.

Can a redirect loop be cached?

Yes. Browsers cache 301 and 308 redirects, sometimes for a long time, so a loop can persist for readers even after it is fixed on the server. Test fixes in a private window or with curl, and prefer 302 or 307 while experimenting with redirect rules.

Filed underFixesCloudflare