Turning Complex Problems Into Confident Technology

Seventy-nine routing rules the platform already ran

Reliability and cost · Production · Inside the Workshop

The problem

This site runs on Azure Static Web Apps, and one configuration file controls its routing, redirects and security headers. Static Web Apps refuses to deploy that file above 20,000 bytes. In October 2026 mine was 19,902. A content change had already had to squeeze its redirects into wildcards to fit, and planned security-header work had nowhere to go.

Separately, an audit had found that some of the site's redirects were throwing away ad click IDs. A visit to /privacy.html?gclid=... landed on /privacy with no gclid. A click ID lost on the way in means the click cannot be tied to anything the visitor did next.

These turned out to be the same problem.

What the rules were doing

Most of the file was rules I had written to give every page a clean address: serve /services from services.html, and redirect /services.html to /services. That is exactly what Static Web Apps already does by itself when its trailing-slash setting is "auto". It serves /page from page.html, and sends /page.html, /page/ and /dir/index.html to the canonical address with a single 301.

There is one difference that matters. When the platform redirects, it keeps the query string. When one of my rules redirects, Static Web Apps sends the rule's target exactly as written, so the query string is dropped. My rules restated the platform's behaviour, and the redirects among them were the ones losing click IDs.

Characterise first

Removing routing rules from a live site by reading the documentation and hoping is how pages go missing. So before removing anything, I recorded what every address did.

Neither the local web server nor the Static Web Apps emulator reproduces production routing, and the dev and test environments sit behind sign-in. So I wrote a small model of production routing as a test, with each behaviour taken either from the platform's documentation or from a production response I had observed directly. From that model and the current config I generated a baseline of 426 paths: every page in every spelling, every route rule, an asset of each type, and some addresses that should miss. Each was requested with gclid, msclkid, fbclid and utm_ parameters attached, so a dropped query would show in the recorded answer. The baseline covered production and the locked-down environments, both anonymous and signed in. The test passed against the unchanged config.

A first trial of the change then caught the model being wrong. It answered /fit.html/ and /index with a 404 once their rules were gone, where production answers them in one platform redirect. I probed production with addresses that have never had a rule, taught the model what the platform does, and regenerated the baseline from the unchanged config. Every existing answer stayed the same, and 64 new paths were added. That sharpened the specification without moving it.

The change

With the baseline in place, 79 rules came out:

  • 54 rewrites that served a clean address from its .html file;
  • 3 folder rewrites that never ran at all, because the platform normalises folder addresses before any rule is applied;
  • 22 redirects from an .html address to its clean form, including /index.html to /.

The file went from 19,902 bytes to 11,612. Nothing else in it was touched.

I measured the alternatives before choosing, each as bytes saved from 19,902. Removing the 54 rewrites saved 5,502, the self-redirects 2,541 and the folder rewrites 247. Putting one rule per line would save 3,595, merging redirect pairs 533, and dropping explicit 302 status codes 175. Converting every page into a folder page would remove the same rules, but would move 54 canonical addresses from /x to /x/. The best combination that kept the rewrites reached 13,713 bytes, and still left a rule to add for every new page and a reformatted file. Removing what the platform already does was smaller and simpler.

Exactly two kinds of change

The baseline test then ran against the new config. It passed with exactly two kinds of difference, and the test names both:

  • 135 answers now keep the query string on the same single 301;
  • 162 answers for /page/ became one 301 to /page, query kept, where the old rewrite had served a duplicate copy of the page with a 200.

The second is what the trailing-slash setting is meant to do, and no internal link uses the slash form. New tests check that every redirect lands in one hop, that every platform redirect keeps the query, and that a rule restating the platform cannot come back. A lint now warns when the file passes 17,000 bytes, so lost headroom is noticed while there is still room to act.

The redirects that remained

Two pieces were still losing the query.

The home page has its own redirect script, which sends a returning visitor straight to their usual audience section. It dropped the query too. So did the deep link used by shared links and QR codes, and the audience chooser's links. Every one of those hops now carries the whole query string, minus only the parameters the home page itself uses. I considered forwarding a list of known click-ID and campaign parameters instead, and rejected it: a list goes stale whenever a platform adds a new ID, and nothing the site does with the rest of the query is harmful to pass on.

The other piece was 36 legacy redirects, such as /services and /pricing, which now lead into the audience chooser. Each sends an old address somewhere genuinely different, so the platform cannot handle them for me, and as config rules they cannot keep a query string. They are now also served at the Cloudflare edge in a form that keeps it, generated from the same config file so the two cannot quietly disagree. The config rules stay as the fallback. A verification run against production passed 155 of 155.

What to take from this

  • Find out what your platform already does. Rules that restate it cost space, and can behave slightly worse than the default they copy.
  • Characterise before you remove. A recorded baseline turns "I think nothing changed" into a list of exactly what did.
  • Check your model against production. Mine was wrong in a way only a real probe showed, and the baseline caught it before anything shipped.
  • Test redirects with a query string attached. A redirect that drops ?gclid= looks perfect in a browser.
  • Prefer passing everything over an allowlist when nothing in the payload is harmful. Allowlists go stale.

Related

If this is your problem too

Configuration that has grown by accretion is easy to leave alone until it hits a limit, and making it smaller without changing what it does is part of production reliability work. What the rest of this site does with a visit is in how this site actually works.

Got a problem like this?

A free 20-minute call: you’ll know whether I can help, what is probably wrong, and what the next step involves. No obligation.

Book a free 20-minute call Describe your problem