EAS Update Published Successfully but Users Still Run the Old Bundle: A Diagnostic Guide
eas update succeeded but users still run the old bundle? Diagnose channel/branch mapping, runtime version mismatches, cold-start timing, and crash rollbacks — and know when OTA can't help.
13 Jul 2026, 05:26 UTC

You ran eas update, the CLI reported success, and yet testers and users keep seeing the old UI, the old bug, and the old crash. This is one of the most common Expo over-the-air (OTA) failures, and it almost never means the publish failed. It means the update and the installed builds disagree about something. This guide walks through the checks in the order that finds the problem fastest.
What the condition looks like
The recognizable pattern: eas update --branch production completes and prints an update group ID, but after reloading the app on a device, nothing changes. Sometimes a few devices update and others don't. Sometimes the update downloads, the app flashes, and then it's running the old bundle again (a rollback after a crash). One important nuance before you start: expo-updates checks for new updates on a cold start, not on background/resume. If you've been testing by swiping the app away from the recent-apps list without fully killing it, force-kill and relaunch before concluding anything is broken.
Symptom-to-cause map
| Symptom | Most likely cause | First check |
|---|---|---|
| No devices pick up the update | Channel/branch misalignment or runtime version mismatch | eas channel:list and eas update:list |
| Some devices update, others don't | Mixed build versions in the wild with different runtimeVersions | Compare Updates.runtimeVersion across devices |
| Update downloads, then app reverts to old bundle | New JS crashes on launch; expo-updates rolls back | Device logs; native API usage in the new JS |
| Update appears after a delay or second relaunch | Normal cold-start check timing or fallbackToCacheTimeout behavior | Force-kill, relaunch twice |
Ordered checks
1. Confirm the publish actually landed where you think
Run eas update:list --branch production (substitute your branch) from your project directory. You need to be logged into the correct EAS account (eas whoami). Confirm your update group exists and note its runtime version and creation time. If it's not listed, the publish went to a different branch or project — check extra.eas.projectId in app.json/app.config.js.
2. Verify the channel → branch mapping
This is the most misunderstood part of EAS Update. Branches are what you publish to; channels are what builds listen to. A channel points at one branch at a time. Your release build embeds a channel (set in eas.json per build profile) at build time — it never changes for that binary. If you published to a branch that the build's channel doesn't currently map to, the update exists but is invisible to those builds.
Run eas channel:list to see which branch each channel points at, and eas branch:list for the reverse view. If the mapping is wrong, repoint it with eas channel:edit rather than republishing.
3. Verify the runtime version matches
An update only reaches builds whose runtimeVersion resolves to the same value. If your app.json uses the appVersion or nativeVersion policy, then bumping your app version (or any native-affecting field, depending on policy and SDK) silently creates a new runtime — your old builds will ignore every update you publish afterward. Confirm the runtime version shown in eas update:list matches the runtime version of the builds in the field.
4. Confirm what the device actually loaded
Don't guess — log it. Add a temporary startup log (or a hidden diagnostics screen) in a release build:
import * as Updates from 'expo-updates';
console.log('channel:', Updates.channel);
console.log('runtimeVersion:', Updates.runtimeVersion);
console.log('updateId:', Updates.updateId);
console.log('isEmbeddedLaunch:', Updates.isEmbeddedLaunch);Run this on a physical device with a release build (OTA does not apply to Expo Go or development builds). If isEmbeddedLaunch is true, the device is running the bundle shipped inside the binary — no update was applied. If updateId shows an older group, the device is on an older update and the new one isn't reaching it, pointing back to checks 2 and 3.
5. Rule out rollback after a crash
If the update downloads but the app reverts, the new JS is likely crashing on launch and expo-updates' error recovery is rolling back to the last known-good bundle. The classic trigger: your new JS calls a native module or API that exists in your dev environment but not in the older binaries in the field. Check device logs (adb logcat on Android, Console app or Xcode devices window on iOS) for a JS exception at startup. Note that automatic rollback mitigates this risk but does not eliminate it — a crash loop before recovery completes is still a bad user experience.
Fixes tied to findings
- Wrong branch mapping: repoint the channel with
eas channel:edit. No rebuild needed. - Runtime mismatch from an intentional version bump: publish the update separately for each runtime version still in the field (
eas updatetargets the runtime resolved from your current config, so you may need to check out the older config to republish for older builds). - Rollback from a crashing update: fix the JS incompatibility and republish; the rolled-back devices will pick up the corrected update on the next cold start.
- False alarm from test method: standardize on force-kill → relaunch → relaunch again before declaring failure.
When to stop debugging OTA and cut a new binary
Escalate to a full store build when the fix itself requires native changes: adding a native module, changing permissions, updating a config plugin, bumping the Expo SDK, or changing any app.json field your runtime policy treats as native-affecting. OTA can only deliver JavaScript and assets that are compatible with the native code already inside the installed binary. No amount of republishing fixes that — build a new binary with eas build and resubmit to the stores.
Verification and caveats
End-to-end check: install a release build on a physical device, publish a trivial visible change (a version label), force-kill, relaunch twice, and confirm the label changes. For extra confidence, deliberately publish an update with a mismatched runtimeVersion in a staging environment and confirm it's ignored — that validates your understanding of your runtime policy before it matters in production.
Caveats: EAS Update CLI flags, policy names, and default behaviors change across Expo SDK releases — verify against the SDK version pinned in your project rather than assuming current defaults. Also, while app store guidelines generally permit OTA updates that don't materially change the app's purpose, interpreting that policy is your responsibility.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.