React Native OTA Updates with EAS Update: a CodePush Alternative
AI generated
RN
native
React Native · Expo · EAS Update · OTA
React Native OTA Updates with EAS Update
a practical CodePush alternative

EAS Update ships JavaScript bundle and asset updates directly to installed React Native and Expo apps, with no new App Store or Play Store review cycle required. Teams coming from CodePush will find EAS Update to be the technically consistent successor, with channels, branches, runtime versions, staged rollouts, and fast rollbacks built in from the start.

18 min read EAS Update · Channels · Branches · Runtime Versions · Rollouts Expo SDK 51+ · expo-updates

1. Why EAS Update makes OTA updates indispensable for React Native and Expo

A classic problem in every React Native app: a small JavaScript bug slips into a release, say a wrong field name in the checkout logic or a crash when opening a particular screen. Without an OTA mechanism, the only path is the regular store route: a new build number, submission to Apple and Google, waiting for review, and only then does the slow rollout to users begin. Apple's review cycle alone can take anywhere from a few hours to several days, which is unacceptable for a critical bug. This is exactly where EAS Update comes in: it ships an updated JavaScript bundle along with assets directly to already-installed apps, with no new store review required.

Technically, EAS Update does nothing mysterious: Metro builds a new JS bundle, which is uploaded together with any changed assets (images, fonts, JSON) to Expo's update servers and stored there as a manifest. On launch, or on an explicit call, the app checks whether a newer manifest exists for its channel, downloads the associated bundle, and applies it on the next restart. The native portion of the app, the compiled iOS and Android code, remains completely untouched. OTA here explicitly means: JavaScript and assets only, never a native binary.

For teams running React Native in production, this is a massive difference in responsiveness. A critical bugfix that would take days in the classic store pipeline can be delivered to all affected users via EAS Update within minutes. That applies equally to copy fixes, broken API calls, faulty business logic, and layout problems, as long as no new native code is required. Anyone coming from the CodePush ecosystem will recognize the underlying principle immediately, but will find a considerably more thought-out model of channels, branches, and runtime versions in EAS Update.

2. Channels, branches, and runtime versions: how the model works together

The core of EAS Update is a three-tier model that looks abstract at first glance but, in practice, solves exactly the problems simpler OTA solutions like CodePush often stumbled on. A branch is a named update stream, comparable to a git branch, for example production or staging. Every publish via eas update lands on exactly one branch. A channel, by contrast, is a layer of indirection: it is baked into the app at build time and determines which branch the app queries at runtime. The channel-to-branch mapping lives in the EAS configuration and can be changed at any time, without requiring a new native build.

This indirection is the real trick: if something goes wrong on the production branch, the production channel can be repointed to another known-good branch within seconds, with no new App Store submission involved. The model is rounded out by the runtime version: it describes which native API surface a given app build exposes. Every JS bundle published via EAS Update also carries a runtime version. Only when the installed app's runtime version exactly matches the update's runtime version is the update even recognized as compatible and downloaded.

This check prevents exactly the scenario that causes crashes in naive OTA approaches: a JS update references a native module that does not exist in an older, still-installed binary. Without a runtime version check, the app would crash the moment that module is invoked. With a correctly configured runtime version, an old binary is simply never offered an incompatible update, it stays on its last compatible version until the user installs a regular store update containing the new native code. The runtime version policy is set in app.json or app.config.js, typically as an appVersion policy that derives the runtime version automatically from the app version number.


{
  "expo": {
    "name": "MyApp",
    "slug": "my-app",
    "version": "3.4.0",
    "runtimeVersion": {
      "policy": "appVersion"
    },
    "updates": {
      "url": "https://u.expo.dev/00000000-0000-0000-0000-000000000000",
      "checkAutomatically": "ON_LOAD",
      "fallbackToCacheTimeout": 0
    },
    "ios": { "buildNumber": "17" },
    "android": { "versionCode": 17 }
  }
}

3. The eas update CLI: publishing updates

Day-to-day work with EAS Update happens almost entirely through the eas update CLI command. The simplest invocation publishes the current JS bundle to a given channel, along with a message that later shows up in the dashboard and in eas update:list. Alternatively, an update can be published directly to a branch, which makes sense in teams running several parallel feature branches before any channel mapping even exists. The --auto flag automatically uses the name of the current git branch as the update branch, saving several manual steps in CI pipelines.

Before every eas update call, the CLI builds the JS bundle locally or in the cloud, computes the runtime version according to the configured policy, and uploads the bundle plus any changed assets to Expo's servers. Important for React Native teams: every published update gets a unique group id, which can later be used to inspect individual updates, republish them, or reference them for a rollback. The eas update:configure command sets up the initial channel-to-branch mapping interactively and should be run once for every new project.


#!/usr/bin/env bash
# Publish a new OTA update to the "production" channel
# The channel routes to whichever branch is currently mapped to it
eas update --channel production --message "Fix checkout crash on iOS 18.1"

# Publish directly to a named branch (channel mapping happens separately)
eas update --branch production --message "Hotfix: null pointer in cart reducer"

# Let EAS auto-detect the branch name from the current git branch
eas update --auto

# List recent updates on a branch, including runtime version and group id
eas update:list --branch production

# Inspect a single update in detail (manifest, runtime version, assets)
eas update:view <update-group-id>

# Configure channel-to-branch mapping interactively
eas update:configure

4. Configuring eas.json: build profiles and channel mapping

eas.json is the central configuration file for both EAS Build and EAS Update. For OTA updates, the most relevant field is channel inside each build profile: it determines which channel a build is permanently wired to once it is installed on iOS or Android. A build with the production profile and production channel only ever queries updates published to whichever branch is currently mapped to that channel. A separate staging profile with its own channel lets internal testers see new OTA updates before they ever reach the production channel.

This separation, in practice, is the difference between a controlled rollout process and a risky blind flight. A team can ship an OTA update exclusively through the staging channel to an internal TestFlight or internal-testing group first, verify there are no regressions, and only then promote the same update group to the production channel. That keeps React Native with EAS Update manageable even under frequent release cadences, because every channel has a clear, isolated target audience.


{
  "cli": {
    "version": ">= 12.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "production": {
      "channel": "production",
      "autoIncrement": true
    },
    "staging": {
      "channel": "staging",
      "distribution": "internal"
    }
  },
  "submit": {
    "production": {}
  }
}

5. Updates.checkForUpdateAsync and fetchUpdateAsync in app code

On the app side, the expo-updates library handles the actual update logic. Two functions are central: Updates.checkForUpdateAsync() asks the update server whether a newer, compatible manifest exists for the current channel, without downloading anything yet. If an update is available, Updates.fetchUpdateAsync() downloads the bundle and assets in the background. The update is not applied immediately, only after a Updates.reloadAsync() call or the next regular app start, so an active user session is never interrupted mid-interaction.

Setting checkAutomatically: "ON_LOAD" in app.json makes expo-updates check for an update on its own at app launch and load it in the background. For a better UX, many teams combine this with a manual check, for example when the app returns from the background, and show the user a prompt instead of restarting the app without warning. A crucial guard is checking against development mode: in __DEV__, expo-updates never returns real updates, a check there would run into a dead end and should be skipped.

Error handling on OTA checks is mandatory, not optional: a user without a network connection should never see an error message just because EAS Update happened to be unreachable. Network errors from checkForUpdateAsync() or fetchUpdateAsync() should be silently caught, the app simply keeps running on the currently installed version in that case.


import * as Updates from "expo-updates";
import { Alert } from "react-native";

// Manual update check, e.g. triggered when the app returns to the
// foreground, or from a "Check for updates" button in settings
export async function checkForOtaUpdate() {
  if (__DEV__) {
    // Never check for updates in development, only in built binaries
    return;
  }

  try {
    const update = await Updates.checkForUpdateAsync();

    if (!update.isAvailable) {
      return;
    }

    // Download the new JS bundle and assets in the background
    const result = await Updates.fetchUpdateAsync();

    if (result.isNew) {
      Alert.alert(
        "Update available",
        "A new version has been downloaded. Restart now to apply it?",
        [
          { text: "Later", style: "cancel" },
          { text: "Restart", onPress: () => Updates.reloadAsync() },
        ]
      );
    }
  } catch (error) {
    // Network errors are expected occasionally, fail silently
    console.log("OTA update check failed", error);
  }
}

6. Staged rollouts: percentage distribution and monitoring

Rolling out an update to a hundred percent of users immediately is risky, no matter how thoroughly it was tested internally. EAS Update therefore supports staged rollouts through a rollout percentage: a new update is initially delivered to only a small share of installs, while the rest keep receiving the previous, known-stable version. Only once error rates and crash statistics for that first cohort look clean is the percentage gradually increased, until every user is eventually on the new version.

In practice, a staged rollout is combined with crash reporting tools like Sentry or Bugsnag: after publishing to ten percent of users, the team watches for a few hours whether the crash-free rate stays stable and no new error patterns show up. If everything stays calm, the rollout is widened to fifty percent and then to a hundred percent. This entire process can be controlled fully through the EAS Update CLI or dashboard, without requiring a new native build or another store review.

Especially for React Native teams with a large user base, this is a critical safety mechanism: a faulty OTA update, at worst, only hits the first small cohort before anyone can intervene, instead of immediately affecting the entire user base. This reduces the risk of an OTA rollout to roughly the level of a normal, server-side feature flag.

7. Rollbacks: getting back to a stable version fast

If monitoring during a rollout shows elevated crash rates or new errors, a rollback needs to happen as fast as possible, ideally without users noticing anything at all. Because every update published through EAS Update carries a unique group id, the previous, known-stable state can be republished to the same channel at any time. This republishing effectively overwrites the faulty state without rebuilding anything, the bundle already exists and merely needs to be marked as the current version for the channel again.

The decisive advantage over a classic store rollback: a faulty native binary can practically not be pulled back from the App Store, a new review cycle would be required even to fix it. A faulty OTA update via EAS Update, on the other hand, can be corrected within minutes, because the entire update logic operates at the channel and branch level and requires no new native compilation. Combined with a preceding staged rollout, the damage from a faulty update is already limited to a small user group before a rollback even needs to kick in.


#!/usr/bin/env bash
# Publish a hotfix to only 10% of installs first
eas update --channel production --message "Staged rollout: payment fix" \
  --rollout-percentage 10

# Watch crash-free rate and error counts in Sentry / EAS dashboard before continuing

# Widen the rollout once the first cohort looks healthy
eas channel:edit production --rollout-percentage 50

# Complete the rollout to 100% of installs
eas channel:edit production --rollout-percentage 100

# Roll back instantly: republish the previous known-good update group
eas update:republish --group PREVIOUS_GOOD_UPDATE_GROUP_ID --channel production

8. The limits of OTA: what EAS Update cannot update

Powerful as EAS Update is, it has a clear, deliberate boundary: anything touching native code can never be shipped over OTA. If a new native module is integrated, say a new camera library, a new Bluetooth SDK, or a changed native permission entry in Info.plist or AndroidManifest.xml, a plain JS update is no longer enough. In that case a new EAS Build is mandatory, followed by a regular submission to the App Store and Play Store with a full review cycle.

App icons, native-level splash screens, native permission requests, and anything requiring a change to the compiled binary itself, all fall outside the scope of OTA. This is exactly why the runtime version check from section two exists: it guarantees that a user on an old binary never receives a JS update that would reach into a native module that does not exist. Instead of a crash, the app simply stays on its last compatible version until the user updates through the store.

For teams, this translates into a clear rule of thumb: pure JavaScript changes, business logic, styling, copy, API calls, and bugfixes within existing native boundaries all belong to EAS Update. New native dependencies, changed native permissions, and anything requiring a new native SDK strictly belong in a new EAS Build followed by a store review. Respecting this boundary is also exactly why Apple tolerates OTA updates in React Native apps at all, see section nine.

9. EAS Update compared: CodePush, app-store-only, and App Store policy

Microsoft finally shut down App Center, and with it CodePush, in early 2025. For teams that had relied on CodePush as their OTA solution for years, this was a forced migration, not an optional upgrade. EAS Update is not an accidental replacement here, it is the natural successor: it comes from the same provider as Expo and EAS Build, is deeply integrated into the Expo workflow, and offers a considerably more robust model with channels, branches, and runtime version checking than CodePush's simpler deployment concept.

An important question for any OTA setup is also what Apple actually allows. Under Apple's App Store review guidelines, interpreted code, such as what is shipped via EAS Update, must not change the primary purpose of the app and must not download features that go materially beyond what was reviewed at initial submission. Bugfixes, copy changes, layout adjustments, and business logic within the existing feature set are explicitly permitted, while entirely new features that change the character of the app, or loading functionality that circumvents rights checked during review, are not. EAS Update already technically respects this boundary through the runtime version coupling to native builds.

Criterion CodePush (discontinued) EAS Update App-store-only release
Review time for bugfixes None (OTA) None (OTA) Hours to days per store
Rollback speed Minutes (server shut down) Minutes, via republish New review cycle required
Native code support No, JS/assets only No, JS/assets only Yes, full native code
Hosting / operations Discontinued since 2025 Hosted by Expo/EAS, actively maintained No separate hosting needed
Runtime compatibility check Manual, error-prone Built in via runtime version Not applicable (always full binary)

The comparison makes clear why EAS Update is currently the obvious choice for OTA updates in React Native and Expo apps: CodePush simply no longer exists as an actively maintained option, pure app-store-only releases are too slow for small fixes, and EAS Update fills exactly the gap in between, without violating Apple's rules.

Mironsoft

React Native, Expo, and mobile deployment pipelines

Want OTA updates for your React Native app set up right?

We set up EAS Update with channels, branches, and a runtime version policy, build staged rollouts with monitoring, and make sure rollbacks land in minutes instead of days when things go wrong.

EAS Update setup

Configuring channels, branches, and a runtime version policy that matches your release strategy

Staged rollouts

Building percentage-based rollout strategies with crash monitoring and clear escalation steps

CodePush migration

Cleanly migrating existing CodePush integrations over to EAS Update

10. Summary

EAS Update ships JavaScript bundle and asset updates directly to installed React Native and Expo apps, with no new App Store or Play Store review required. The channel-to-branch mapping lets teams redirect the update flow at any time, while the runtime version check reliably prevents an old binary from receiving an incompatible OTA update and crashing. Through the eas update CLI and eas.json, publishing, staged rollouts, and rollbacks can all be controlled without a new native build.

The boundary remains clear: anything involving native modules, native permissions, or changes to the compiled binary still requires a new EAS Build and a regular store review, which is also exactly the frame Apple's rules set for OTA content. With CodePush gone as of 2025, EAS Update is the consistent, actively maintained successor for React Native and Expo teams that need fast, safe OTA updates.

EAS Update for React Native, OTA updates at a glance

What OTA can do

Ship the JS bundle and assets directly to installed apps, with no App Store or Play Store review, in minutes rather than days.

Channels, branches, runtime versions

A channel routes to a branch at runtime, the runtime version prevents incompatible updates from reaching old native binaries.

Staged rollout & rollback

Increase rollout percentages step by step, watch crash rates, and roll back a faulty update via republish within minutes.

Limits of OTA

New native modules, changed native permissions, and binary changes strictly require a new EAS Build plus a store review.

11. FAQ: EAS Update and OTA Updates in React Native

1What exactly is EAS Update?
Expo's OTA update service: ships new JS bundles and assets directly to installed React Native apps, with no new store review.
2Channel vs. branch?
A branch is the update stream that gets published to. A channel is baked into the app and determines which branch it queries at runtime.
3What is a runtime version?
Describes a build's native API surface. Updates are only shipped on an exact match, preventing crashes from missing native modules.
4Add native modules via EAS Update?
No, that always requires a new EAS Build and a regular store review. EAS Update only updates JavaScript and assets.
5How fast does an update land?
Available immediately after publishing, typically checked and applied automatically on the next or following app launch.
6When to use a staged rollout?
For any update carrying risk, especially critical business logic like checkout, to limit the blast radius of a faulty release to a small cohort.
7How does a rollback work?
The previous update group is republished to the same channel via eas update:republish, no new native build, usually within minutes.
8Is CodePush really dead?
Yes, Microsoft shut down App Center including CodePush in 2025. EAS Update is the actively maintained, technically more robust successor.
9Does Apple allow OTA updates?
Yes, as long as the app's primary purpose stays unchanged and no features beyond the reviewed scope are loaded.
10New review needed per OTA update?
No, that is the core advantage. Without native changes, delivery happens entirely without a new App Store or Play Store review.