The Cloudflare Pages documentation tells you the limit is 2,000 redirects. It is not lying, but it is pointing at the wrong number. The limit that took critical redirects off this site in production was the 100KB file-size cap, and the failure mode is the worst kind: silent. No build error. No deploy warning. No log line. The _redirects file uploads, the deploy goes green, and a chunk of your rules simply never apply.
This is a polemic with the way the limit is taught. “Stay under 2,000 rules” is the advice everyone repeats, and it is the advice that let our file grow to about 157KB while still nowhere near 2,000 rules. The number that mattered was never in the sentence we were watching.
The Cloudflare _redirects byte cap: TL;DR in 4 points
- Cloudflare Pages caps
_redirectsthree ways: 2,000 static rules, 100 dynamic rules, and 100KB of file size. The first is the one everyone quotes; the third is the one that breaks production. - Rules past the 100KB byte cutoff are dropped when the file is processed at deploy time. There is no warning. The deployed file just stops applying rules at some line.
- A six-locale site with long descriptive slugs crosses 100KB around 1,200 to 1,300 rules, far below the rule-count limit. So you hit the byte wall while the dashboard still says you have headroom.
- The fix is not “delete redirects.” It is to move bulk one-to-one rules into a Pages Function, which is bounded by a 1MB compressed script size, not the 100KB redirects cap.
Glossary: _redirects, Pages Functions, splat, 301
This case rests on a few platform concepts. If any is unfamiliar, it is worth thirty seconds, because the failure hides exactly in the gap between them.
_redirects- a plain-text file in the build output (herepublic/_redirects) where each line is one rule: a source path, a destination, and an optional status code. Cloudflare Pages reads it at deploy and applies the rules at the edge before your site is hit.- Static redirect - a literal one-to-one rule, for example
/old-page/ /new-page/ 301. No wildcards. - Dynamic redirect - a rule with a wildcard
*or a:splatplaceholder, for example/blog/* /articles/:splat 301. These use Cloudflare’s path-template engine and are capped at 100 per file. - 301 - the HTTP status for a permanent redirect. It tells Google the old URL has moved for good and the ranking signal should follow to the new URL.
- Pages Function - a small serverless script (here under
functions/) that runs at the edge on every matching request. It can read a lookup table and issue redirects in code, with no 100KB ceiling. _middleware.ts- the Pages Function that runs on every request before routing. The natural place to do a map lookup and 301 if the path matches.
These distinctions are invisible until a redirect stops firing. Then they are the whole story.
How a 157KB _redirects file broke production
The file did not grow recklessly. It grew the way every long-lived multilingual site’s redirect map grows: each time a slug was cleaned up, a stale URL retired, or a locale variant reorganized, a (old, new) pair was appended. Six locales, descriptive slugs like /pl/tworzenie-stron-internetowych-oraz-sklepow-wordpress/, and a few years of housekeeping put the file at roughly 157KB and about 1,890 rules.
Nobody was worried, because 1,890 is under 2,000. That was the mistake. The byte cap had been crossed long before the rule cap was anywhere close.
The symptom did not look like a redirect problem at first. It looked like three unrelated fires:
- Google Merchant Center flagged two of twelve products as unavailable product pages. Their feed destinations were redirects that sat near the bottom of the file.
- Google Search Console reported 388 “Not found (404)” URLs. A large share of them had
_redirectsentries that, on paper, should have caught them. - GSC validation kept failing on repeat. We would request validation, Google would revisit the URL, hit the same 404 because the redirect was never live, and reject the validation. The loop did not break on its own.
The pages, the templates, the build, the rule count all looked healthy. The routing layer was quietly amputated below a line nobody was looking at.
How to confirm the tail is being dropped
The diagnostic is mechanical once you stop trusting the file and start trusting the edge. Do not sample rules at random; sample them by position.
- Take a rule from near the top of
_redirects, one from the middle, and one from near the bottom. - Request each source path in production and read only the status code:
curl -s -o /dev/null -w "%{http_code}" https://your-site/source-path/. - Compare. On this site, rules around line 9 to 130 returned
301cleanly. Rules past roughly line 200 returned404, with a few false passes where unrelated middleware logic (cross-language slug detection, legacy patterns) happened to catch the URL by another path.
When the top of the file works and the tail 404s while both rules are committed and identical in shape, the conclusion is not ambiguous: the file is being truncated at the byte cap. The exact cutoff line depends on how long your average rule is, which is why a six-locale site with long slugs hits it sooner than the rule count suggests.
Why the byte cap is the binding limit, not the rule count
| Cap | Documented value | What it counts | When it bites |
|---|---|---|---|
| Static redirects | 2,000 rules | Literal one-to-one lines | Large sites, eventually |
| Dynamic redirects | 100 rules | Lines with * or :splat | Heavy wildcard use |
| File size | 100KB | Total bytes of the file | Long URLs across many locales, early |
The arithmetic is the whole argument. A short rule like /a/ /b/ 301 is about 12 bytes. A realistic multilingual rule, /pl/tworzenie-stron-wordpress/ /pl/tworzenie-stron-internetowych-oraz-sklepow-wordpress/ 301, is closer to 90 bytes. At 80 to 90 bytes per rule, 100KB is exhausted around 1,200 to 1,300 rules - hundreds short of the 2,000 the documentation foregrounds. The cap you watch should be the one you hit first, and for descriptive-slug sites that is bytes, not lines.
What the Cloudflare docs say, and what they leave out
The redirects documentation does list all three caps. The 100KB figure is there in writing. What the page does not say, anywhere, is what happens when you exceed it: the rules past the cutoff are dropped, and the deploy succeeds anyway. The contrast with the rule-count limit matters. Exceed 2,000 rules and the build surfaces it; the file is too big in a way the platform tells you about. Exceed 100KB and the platform truncates and stays quiet.
That asymmetry is the trap. A limit that errors loudly trains you to respect it. A limit that fails silently trains you to ignore it, because nothing ever goes red. The same problem class shows up in the sibling _headers file, which carries its own size constraints, and in any build artifact where “it deployed” is mistaken for “it all applied.” The platform contract you should internalize is not the largest number on the limits page; it is the smallest number that fails without telling you. The same trust gap underpins our earlier write-up on how AI translation breaks multilingual SEO at the structural layer: in both cases the visible output looks correct while the routing layer underneath is broken.
The fix: move bulk redirects into a Pages Function
_redirects is the wrong storage for thousands of literal rules. The right storage is a Pages Function reading a map. Functions are limited by compressed script size, up to 1MB, not by the 100KB redirects cap, so a 200KB redirect map fits with roughly five times the headroom.
The migration on this site, shipped in a single commit on 2026-05-07, had three parts:
functions/redirect-map.tsexports aReadonlyMap<string, string>of source-to-destination pairs. Keys are normalized once - lowercased, trailing slash added, repeated slashes collapsed - so each request is a single O(1) lookup, not a scan. It started at 1,851 entries and now holds 2,233.functions/_middleware.tsperforms the lookup between the existing normalization steps (slash collapse, diacritic asciification) and the trailing-slash step. It builds the normalized key, checks the map, and issues a direct 301 to the target when it hits.public/_redirectswas trimmed from about 157KB to roughly 3.3KB and 41 rules: a header comment, four top-priority Merchant Center rules, and the wildcard or:splatrules that genuinely need Cloudflare’s path-template engine.
Thirty rules sampled at random from the new map all returned 301 cleanly in production. The 404 loop in Search Console ended once the redirects it kept expecting actually existed at the edge.
Middleware order is part of the fix
Moving the rules into a Function is only correct if the lookup runs at the right point in the request. _middleware.ts already did real work before the migration: it lowercases and collapses repeated slashes, asciifies diacritics so /pl/wdrozenia/ and a diacritic-laced variant resolve to the same route, and adds a trailing slash. The map lookup has to sit after normalization and before the trailing-slash redirect, or two bugs appear.
Put the lookup too early, before slashes are collapsed, and a key that was normalized at build time (lowercase, single slashes, trailing slash) never matches a raw inbound path with a double slash or mixed case. The map silently misses and the URL falls through to a 404, which looks exactly like the bug you were trying to fix. Put it too late, after the trailing-slash redirect has already issued a 301, and you pay two hops for every redirected request: one to add the slash, one to reach the destination. Two-hop redirects bleed link equity and slow the crawl. The map must consume the already-normalized key and short-circuit with a single 301. Edge-routing logic is sequential, not a bag of independent rules, and the order is as load-bearing as the rules themselves. More edge and platform write-ups sit on the wppoland blog.
Alternatives we rejected, and why
A static map compiled into the Function was not the only option. Two others were on the table.
- Workers KV. Store the redirect pairs in a key-value namespace and look them up at runtime. This scales to millions of entries and decouples redirects from deploys. We rejected it for this site: a KV read adds network latency to every request that might be a redirect, the free-tier read limits are a real ceiling at our traffic, and 2,200 rules fit comfortably in a compiled map that ships with the Function and costs nothing at request time. KV earns its keep when the redirect set is large enough or volatile enough that redeploying to change it is painful. Ours is neither.
- Splitting
_redirectsacross multiple smaller files. Cloudflare Pages reads one_redirectsfile; there is no include mechanism, so this was never real. The instinct behind it - keep using the platform-native feature - is the instinct that let the file grow past the cap in the first place.
The compiled map won because it removes the cap from the equation entirely while keeping lookups in-memory and zero-cost per request. The decision is not universal; it is right for a redirect set in the low thousands that changes a few times a month.
How to keep the redirect layer healthy afterwards
The migration is not a one-time escape; without a discipline the file creeps back. The rules that have held since:
- New static one-to-one redirects go in the Function map, never in
_redirects. Append tofunctions/redirect-map.ts. - New wildcard or
:splatredirects go in_redirects, which now has roughly thirty times its previous headroom under the cap. - Watch the byte size, not the rule count. A line in CI that fails the build if
public/_redirectscrosses about 50KB gives a wide margin before the silent 100KB wall. Rule count is a vanity metric here; bytes are the contract. - Spot-check by position after any large redirect change. Top, middle, tail. It takes three
curlcalls and catches truncation before Google does.
The deeper lesson outlives Cloudflare specifically. A platform limit that fails silently is more dangerous than a hard limit that errors, because the green deploy trains you to trust a file that is no longer fully applied. When a limit is documented as one number and enforced as another, the gap is where production breaks. Read the limits page for every number on it, then verify the one that fails quietly against the live edge, not the committed file.



