Expo OTA Updates with EAS Update: Runtime Versioning, Rollback, and the Native Boundary
OTA updates let you fix shipped JavaScript without a store review, but only inside the native boundary. How runtime versioning and rollback actually work.
29 May 2026, 16:14 UTC

A JavaScript typo shouldn't cost you a store review cycle
A shipped React Native app has a wrong price formatter, or a null check that crashes on one screen. The fix is one line of JavaScript, but the binary is already in the App Store and Play Store. Historically that means cutting a new build, submitting it, and waiting for review while the bug stays live.
Expo's over-the-air (OTA) update path exists for exactly this case. The thesis here: OTA updates trade store-review latency for a hard boundary — you can ship JavaScript and assets, and nothing else. Teams that internalize that boundary get fast fixes; teams that fight it get confusing runtime errors.
Two pieces: the library inside your app and the service that serves it
expo-updates is the library compiled into your app. It downloads a bundle, verifies it, and swaps it in for the next launch. EAS Update is Expo's hosted service that stores bundles and hands them out. They are separate concerns: the library can talk to any compatible update server, but most teams use EAS Update because the CLI is wired into it.
An update can contain JavaScript and assets such as images and fonts. It cannot contain native code, new native modules, or changes to Info.plist or AndroidManifest.xml. If your fix needs a new native dependency, OTA is not the tool.
Runtime versioning is the safety interlock
Every store build carries a runtimeVersion string. The update server only serves a bundle whose runtimeVersion matches the one baked into the installed binary. That is what stops a JavaScript update from calling into a native module the binary does not have.
Expo supports more than one policy for computing that value — commonly a policy tied to the app version, or a fingerprint policy that hashes native dependencies and configuration. Whichever you choose, the rule is the same: the value must change when native code changes, and stay stable otherwise. Changing it silently orphans every installed build, because no update will match them anymore.
Channels connect installed builds to branches. A build configured for the production channel receives updates published to the branch that channel points at, which is how you stage a fix on a preview channel before promoting it.
Worked example: publish a one-line fix, then roll it back
Run these from your project root, on a machine authenticated to the Expo account that owns the project. The commands are version-sensitive — confirm flags with eas update --help and eas update:rollback --help before using them in a real release process. Nothing here was executed for this post; treat it as a shape to verify in your own project.
- Make the change in your JavaScript, for example correcting a string in a screen component.
- Publish it to a branch.
productionand the message text are placeholders:
The command uploads the bundle and prints an update ID. No new binary is built or submitted.eas update --branch production --message "Fix price formatter on checkout" - Verify the publish landed:
eas update:list --branch productionshould show the new entry with its runtime version. - On a device running a release build, background and foreground the app (or fully restart it) so the client performs its check, then confirm the fix appears. Reading
Updates.updateIdfrom theexpo-updatesJS API tells you which bundle is actually running. - If the fix regresses something, roll the branch back:
This repoints the branch to a previous update, or to the binary's embedded bundle, depending on the target you select. Restart the app and confirmeas update:rollback --branch productionUpdates.updateIdchanges back.
For custom UX, the JS API exposes checkForUpdateAsync, fetchUpdateAsync, and reloadAsync, so you can check on demand and prompt the user to restart instead of reloading under them.
Limits, policy, and the checks that catch mistakes
- Delivery is not instant. Clients fetch on their configured schedule — typically at launch or on foreground — so a user may not see the fix until their next session. Avoid promising push-like immediacy.
- Failure falls back quietly. If a download fails or the runtime version does not match, the app keeps running the embedded bundle. That is good for stability and awkward for debugging: "my update didn't show up" is often a runtime version mismatch, not a network problem.
- Store policy applies. Both Apple and Google permit over-the-air updates of interpreted code within limits, and an update must not fundamentally change what the app does. Policy text is revised periodically, so read the current developer policy pages rather than relying on a summary.
- Expo Go, development builds, and release builds behave differently. Verify OTA behavior in a real release build before documenting it for your team.
Release checklist
- Pick a runtime version policy and write down when it must change.
- Map channels to branches in app config, and keep a preview channel for staging.
- Test the full loop once per release train: publish a trivial change to a release build, confirm it applies, then roll it back.
- Record the known-good update ID for each shipped release so rollback has a target.
- Re-run
eas update --helpand the rollback help after upgradingeas-cli; flags and subcommands drift between releases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.