◉RORK LABJP
●GPT6.1 — GPT-6.1 Sol joined the Rork model menu (Sep 29). It is the latest entry on the changelog●10/12 — 7 days left until React Native 0.88.x is due. Expo SDK 58 stable is only described as early October●NEW — Multiseat Purchases Are Already On: Decide Whether to Keep Them Before October 22●SDK 58 — A fix PR (#50998) for npm install failing in new projects is under review. No stable date has been given yet●SONNET — Claude Sonnet 5.5 is now in Rork (Sep 28). It is described as over 30% faster than Sonnet 5●iOS 27 — From April 2027, App Store uploads require the iOS 27 SDK. There is time to update your build environment calmly●GPT6.1 — GPT-6.1 Sol joined the Rork model menu (Sep 29). It is the latest entry on the changelog●10/12 — 7 days left until React Native 0.88.x is due. Expo SDK 58 stable is only described as early October●NEW — Multiseat Purchases Are Already On: Decide Whether to Keep Them Before October 22●SDK 58 — A fix PR (#50998) for npm install failing in new projects is under review. No stable date has been given yet●SONNET — Claude Sonnet 5.5 is now in Rork (Sep 28). It is described as over 30% faster than Sonnet 5●iOS 27 — From April 2027, App Store uploads require the iOS 27 SDK. There is time to update your build environment calmly
Articles/Dev Tools
⬡ Dev Tools/2026-08-10Advanced

Switching to Signed URLs Killed My Image Cache — Decoupling expo-image Keys from the URL

Signed URLs rewrite their query string on every expiry, so a URL-keyed cache never hits. Here is how to pin the key with source.cacheKey, what the cache read and write APIs actually return, and what changed when I re-ran the measurements.

Rork575expo-image7signed URLscache designReact Native238

✦ Premium Article

The week after I moved image delivery behind signed URLs, one graph moved: bandwidth. Same catalog, same resolutions, same screens. The only thing that changed was the shape of the URL.

The culprit was the cache key. A signed URL rewrites X-Amz-Signature and X-Amz-Date every time the expiry rolls over. To the library, that is not yesterday's image. It is an image it has never seen.

As an indie developer running image-heavy wallpaper apps, I spent an embarrassing amount of time raising the disk cache ceiling before I understood this. Raising the ceiling does nothing. The cache was not full. It was never being hit.

Updated 2026-09-20. I went back through this article against the Expo API reference and found two things I had gotten wrong: the call shape of writeToCacheAsync, the return type of readFromCacheAsync, and — separately — the skew exponent I quoted alongside my own measurements. All of it is rewritten below, and I have left the corrections visible rather than editing them out of existence.

Every new signature makes the same image a different image

A signed URL attaches proof to an object path: this key, this operation, valid until this moment. Because the proof is bound to an expiry, it has to be regenerated once that expiry passes.

So the same wallpaper, w042.webp, shows up like this across sessions:

Session 1: https://cdn.example.net/wallpapers/w042.webp?X-Amz-Expires=3600&X-Amz-Date=20260810T000000Z&X-Amz-Signature=6f1c...
Session 2: https://cdn.example.net/wallpapers/w042.webp?X-Amz-Expires=3600&X-Amz-Date=20260811T000000Z&X-Amz-Signature=a93e...

The path is identical. The string is not. That gap is the whole problem.

ComponentBehavior across sessionsSafe to key on?
HostUsually stable, but changes when you move CDNsNot on its own
Path (/wallpapers/w042.webp)Stable. Expresses object identityYes
X-Amz-Date / X-Amz-SignatureChanges on every expiry, by designNever
Transform params (?w=1080&fm=webp)Changes when the request changesYes. Different variant, different key

The key should encode which object, in what shape — never who authorized it, and when.

A URL is the delivery slip. The key is the shelf number. The slip gets reprinted on every pickup; there is no reason to renumber the shelf along with it.

Where the default cache key actually comes from

expo-image derives its on-disk location from the source.uri string. cachePolicy selects which layers store the bytes, memory or disk. It does not change how the key is built. Conflating the two is how you end up with cachePolicy="memory-disk" set correctly and nothing being reused.

// This picks a storage layer. It has zero influence on key stability.
<Image
  source={{ uri: signedUrl }}   // the string changes every session
  cachePolicy="memory-disk"     // bytes get stored, then never looked up again
  style={styles.thumb}
/>

The bytes do land on disk. They simply become entries nobody will ever request again, consuming your disk budget for nothing. The bloat side of that story is covered in Your Rork App's "Documents & Data" Keeps Growing, but when signed URLs are involved, tuning the ceiling will not help. The problem is the key, not the capacity.

✦

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 pinpoint why your bandwidth climbed after moving to signed URLs, and decide exactly which layer to fix
✦You get the real signatures of source.cacheKey, writeToCacheAsync, readFromCacheAsync and getCachePathAsync, and know which one to reach for
✦You can estimate how much of your hit rate a key redesign would recover, by fitting the skew exponent from your own view logs
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.

or
Unlock all articles with Membership →
Share

Thank You for Reading

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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links