Shipping a Hotfix in Minutes: A Practical Look at Expo EAS Update
EAS Update lets you push JavaScript fixes to installed Expo apps without store review. Here's how runtime versions keep OTA updates safe, a staged rollout example with rollback, and where the model breaks down.
20 Apr 2026, 18:59 UTC

Your app is live, a user reports a broken checkout button, and the fix is a one-line JavaScript change. A full store release means a new build, App Store review, and days of waiting. Expo's EAS Update exists for exactly this gap: it lets you push JavaScript and asset changes to installed apps over the air (OTA), without resubmitting a binary — as long as the native code hasn't changed.
This post walks through how EAS Update actually decides which updates a device receives, a worked example of a staged rollout with rollback, and where the model breaks down.
How EAS Update decides what your phone gets
The core concept is the runtime version. Every build you produce with EAS Build embeds a runtime version — a string that identifies the native capabilities of that binary. When the app launches, expo-updates asks the update server for an update matching its runtime version and channel. If your update was published against a different runtime version, the server simply never serves it to that build. This is the guardrail against the classic OTA failure mode: a JS bundle calling a native API the installed binary doesn't have.
On top of runtime versions sits a branch/channel model. Channels are pointers attached to builds (for example, a build points at the production channel), and branches hold the published updates. Publishing an update to a branch makes it available to every build whose channel maps to that branch.
Typical setup, run from your project root with the EAS CLI installed and an Expo account logged in:
npx expo install expo-updates
npx eas update:configure
This adds an updates.url to your app config and a runtimeVersion policy. The appVersion policy ties the runtime to your app's version string; the fingerprint policy (newer SDKs) derives it from a hash of your native dependencies, which catches native changes automatically. The fingerprint policy is the safer default if you're on a recent SDK, because it makes it hard to accidentally publish JS that assumes native code that isn't there.
Worked example: a staged rollout of the checkout fix
Say your production build is on the production channel and you've fixed the button. The naive move is publishing to everyone at once:
npx eas update --branch production --message "Fix checkout button handler"
A safer move is a staged rollout. EAS Update supports publishing to a percentage of users, so you can expose the fix to 10% of devices first:
npx eas update --branch production --message "Fix checkout button handler" --rollout-percentage 10
Devices deterministically fall inside or outside the rollout bucket, so a given user consistently gets (or doesn't get) the update rather than flapping between versions. Watch your crash reporting and analytics at 10%. If it looks clean, publish again at 100%.
If the fix makes things worse, rollback is a branch operation, not a new publish: repoint the production branch to the previous update. Devices pick up the reverted bundle on their next update check (by default, at app launch). That means rollback is fast, but not instant — users with the app already open keep the bad bundle until they relaunch or your code triggers a check.
Checking for updates yourself
The default launch-time check is fine for most apps, but expo-updates exposes an API if you want control — for example, checking when the app returns to the foreground:
import * as Updates from 'expo-updates';
async function checkForUpdate() {
const result = await Updates.checkForUpdateAsync();
if (result.isAvailable) {
await Updates.fetchUpdateAsync();
// Prompt the user, then:
await Updates.reloadAsync();
}
}
The trade-off of a custom flow is that you now own the edge cases: failed downloads, interrupting the user mid-task with reloadAsync(), and deciding how often to check. The default behavior handles the common case with none of that code.
Where OTA updates stop working
EAS Update ships JavaScript and assets only. It cannot change:
- Native dependencies or native code (a new Expo module, a config plugin change)
- Permissions, entitlements, or anything in the app manifest/Info.plist
- The Expo SDK version itself
Any of those require a fresh EAS Build and a store submission. This is also where runtime version mismatches bite: if you bump a native dependency, forget, and publish an OTA update, the fingerprint runtime version changes, and your existing builds silently receive nothing. When an update "isn't showing up," the first thing to check is that the published update's runtime version matches the build's — visible in the EAS dashboard or via the CLI.
Two more honest limitations. First, EAS Update is a hosted service with usage-based pricing tiers; self-hosting the update protocol is possible but you take on the server, signing, and availability burden yourself. Second, store policies: Apple and Google permit OTA updates that fix bugs or tweak behavior, but using them to materially change the app's purpose after review violates their guidelines. Ship fixes, not trojan horses.
A practical pre-publish checklist
- Confirm the update contains no native dependency or config plugin changes (
git diffonpackage.jsonand app config). - Verify the runtime version policy in your app config matches how your builds were produced.
- Publish to a small rollout percentage first; watch crash-free sessions before going to 100%.
- Know the previous update ID so rollback is a single branch repoint, not a scramble.
Used this way, EAS Update turns the worst-case mobile scenario — a trivial bug held hostage by store review — into a ten-minute fix. Just respect the runtime boundary, because the moment your fix needs native code, you're back in the review queue like everyone else.
Note: CLI flags, config fields, and rollout syntax vary across Expo SDK releases. Verify the commands above against the expo-updates and EAS Update documentation for the SDK version your project targets before relying on them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.