●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
301 or Internal Rewrite for Wallpaper Resolution Routing? Inside One Host, I Chose the Route That Doesn't Change the URL
When one host picks an image resolution for you, should it answer with a 301 or rewrite internally? I ran both on nginx and compared round trips, rule-order traps, and a small verification script.
I was tidying image delivery for a wallpaper app. The app asks for an image like sunset.jpg?w=640, adding the width it wants. On the server there are folders split by width. The glue between the two is a routing rule, and I stalled on one question: write it as a 301, or as an internal rewrite?
Both serve the image. That is exactly why it is easy to pick one without saying why. Here is the conclusion first: when the routing stays inside one host and the caller has no need to know about it, keep the URL unchanged. What follows is the same decision reproduced on a local nginx, with both versions actually run.
What needs routing
Here is the setup on one page.
URL requested
File to return
/img/sunset.jpg?w=640
/img/w640/sunset.jpg
/img/sunset.jpg?w=1280
/img/w1280/sunset.jpg
/img/sunset.jpg (no value, or an unexpected one)
/img/sunset.jpg (the original)
For the test I placed dummy files of 2,000 bytes (640 wide), 8,000 bytes (1280 wide), and 30,000 bytes (the original). Different sizes tell you at a glance which file came back.
$ curl -si "http://127.0.0.1:8081/a/img/sunset.jpg?w=1280" | sed -n '1p;/^Location/p'HTTP/1.1 301 Moved PermanentlyLocation: http://127.0.0.1:8081/img/w1280/sunset.jpg
Following the redirect gives num_redirects of 1, so fetching one image costs two round trips, and the access log grows two lines per image. After 300 calls the log held exactly 303 lines with a 301 status (the extra three are my manual checks).
There is a second point that is easy to miss. In the HTTP specification (see the 301 entry in RFC 9110), a 301 is cacheable by default. Browsers remember "this URL moved there," and a corrected rule may not reach a client that already remembered the old answer. That is why you verify in a private window. This part is a reading of the spec; I did not measure how long any given browser keeps it.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can tell with a single curl whether your image delivery changes the URL or keeps it
✦You can take home a resolution router built from map and rewrite, with no if, that falls back to the original for unexpected values
✦You can avoid shipping a 301 that browsers and CDNs remember and that you can no longer fix
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
One round trip, zero redirects, and the byte counts match what I expected. The X-Served-From header shows /img/w640/sunset.jpg. The URL the caller sees never changed, while the file behind it did.
Side by side
Same conditions, viewed by aspect.
Aspect
301 (A)
Internal rewrite (D)
Round trips per image
2 (the 301, then the file)
1
Caller's URL
Changes (moves to the Location target)
Stays the same
After you fix the rule
Clients and CDNs may keep the old answer
Takes effect from the next request
How to verify
Needs a private window
A plain curl is enough
Fits when
A URL moved for good, or you send callers to another host
You switch where the file lives on the same host
The last row is why I chose the rewrite. Resolution routing is not a move; it is a storage detail. There is nothing to tell the caller, and if there is nothing to tell, I never create the extra round trip or the "remembered answer" worry in the first place.
The opposite cases belong to the 301: moving from an old domain to a new one, or telling search engines which URL is the official one. The line is not which is better. It is whether the routing is a move, or a storage detail.
The order trap: first match wins
One more thing I only understood after running it is the order of regex location blocks. nginx reads regex locations in the order written and uses the first match. Put a broad rule first and the narrower one below it never fires.
I ran the same two lines in both orders on separate ports.
narrow-first: narrowbroad-first: broad
nginx -t reports "syntax is ok" for both. A valid syntax and a rule that actually matches are different things. When you add a rule, put the narrower one above. I wrote that line at the top of my own checklist.
Two traps I hit during the test
Both happened while I was running this.
The first: rewrite overwrites named captures. I first captured f in location ~ ^/d/img/(?<f>.+)$ and wrote rewrite ^ $dir/$f last;. Every call returned 404. The rewrite has its own regex that matches anew, so the $f from the location was no longer usable. The fix is to capture again inside the rewrite.
# NG: the rewrite's own regex (^) overwrites $f and the path comes out emptylocation ~ ^/d/img/(?<f>.+)$ { rewrite ^ $dir/$f last; }# OK: capture again inside the rewritelocation ~ ^/d/img/ { rewrite ^/d/img/(.+)$ $dir/$1 last; }
The second: the previous nginx was still running. After editing the config I ran nginx -s stop, but I had started it with a different config file, so it never stopped. The new process could not bind the same port, and the old config kept answering. The error log said bind() ... failed (98: Address already in use), and that is when I noticed. If behavior does not change after you change the config, check which process is running with which config file before you suspect the rule.
A verification script
Here is a script that checks the table above mechanically. It does not follow redirects, so a 301 stands out immediately.
Pointed at the 301 version (A), all four rows come back as 301 with 178 bytes. Against the rewrite version (D), you get the 200s and file sizes shown earlier. If you keep the same rule in several places (multiple servers, or separate storage backends), run the script once per place. When you fix one, make the same fix in every copy of the rule. Writing that into the procedure avoids the quiet mismatch where only one side is stale.
The order I check things in
Before writing any routing rule, I check three things in this order.
Is the URL an "address" the caller should remember? If it moved for good, use a 301; if it is a storage detail, rewrite internally.
Does the target live on the same host? If you are sending callers to another host, a redirect is your only option.
Where do unexpected inputs land? A 404, or the original image? For wallpaper delivery, where "something showing" beats "nothing showing," I recommend writing the original as the default.
Count the round trips
A list screen with 40 thumbnails makes 80 requests with the 301 version and 40 with the internal rewrite: a 50% reduction. Fewer trips from the device also shortens the wait before images fill in as you scroll. This is an arithmetic estimate, not a measured time on a real device.
What to settle before production
When you fix a rule in production, prepare the way back first. Keep the previous config file with a date, and write the restore command into the procedure. An internal rewrite takes effect immediately, which means a mistake takes effect immediately too.
Before you publish
An unexpected w value, and a call with no w, both return the original with a 200
Rules are ordered narrow-first (nginx -t cannot tell you this)
Any capture you need is taken again inside the rewrite
The running process was restarted with the new config
Every location that holds the same rule got the same fix
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.