Over-the-air (OTA) updates let mobile apps download new JavaScript bundles, assets, or content directly from a server, without a new App Store or Google Play release. Users get fixes and changes the next time the app restarts, or immediately if it's a required update. This article covers app-level OTA updates, not the firmware updates that phones and cars receive.
What are OTA updates?
OTA updates are code and content changes delivered straight to an installed app over the internet. For mobile apps, that means JavaScript, assets, and configuration, not native code. Instead of packaging a fix into a new binary and waiting for store review and user adoption, you publish an updated bundle to a server, and the app pulls it down on its own.
The term causes confusion because it first came from somewhere else. Firmware OTA is how phones, cars, and IoT devices receive operating system updates from the manufacturer. App-level OTA is different because it updates the code your app runs inside the binary that's already installed.
Not every app can use OTA updates. The technique works where the app executes interpreted code or renders remote content: React Native and Expo apps (which run JavaScript bundles), Flutter apps through patch tooling such as Shorebird, and apps built around web-view content. Fully native Swift or Kotlin code compiles into the binary itself, so it can't ship this way.
For React Native teams, the most widely used implementation is CodePush, which we cover in depth in our CodePush guide.
How do OTA updates work?
An OTA system has two halves: an update server that hosts versioned bundles, and an SDK inside your app that checks for them. You publish a bundle targeted at a specific binary version, the app detects it on launch or resume, downloads it (often as a diff), and by default applies it on the next restart.
The lifecycle runs from publish to rollback:
- You build a new bundle and publish it to the update server, targeted at a specific binary version (say, everything running 2.4.x).
- The app checks in with the server on launch or when it resumes from the background, reporting its binary version and current bundle version.
- The server compares versions and, if a newer bundle applies to that binary, tells the app where to get it.
- The app downloads the bundle, often as a diff against what it already has, which keeps the payload small.
- By default the app applies the new bundle on the next restart, so users don't see a mid-session swap. That part is configurable per release. CodePush, for example, offers install modes for next restart, next resume, or immediate, and updates you flag as mandatory use immediate, which reloads the app there and then.
- If something goes wrong, a server-side rollback republishes the previous bundle as a new release on the same deployment, with no client release and no store review.

Get the version targeting in step 1 right. A JavaScript bundle built against binary 2.4 may call native modules that don't exist in binary 2.3, and shipping it to the wrong audience produces crashes you can't debug from a stack trace alone.
Most teams automate the build-and-publish half in a CI workflow, so releasing an OTA update becomes a push-to-branch event rather than a manual ritual. Our mobile CI/CD guide covers how to set that up.
Why OTA updates matter for mobile development
OTA updates reduce the time between finding a bug and fixing it for your users. A native hotfix means a new build, a store submission, a review, and then waiting for users to actually update. An OTA fix skips all of that for JavaScript-level changes: you publish once and the change is reflected within hours.
The release-cycle math explains why teams adopt this. Apple reports that on average, 90% of App Store submissions are reviewed in less than 24 hours. But averages hide part of the story. After approval, adoption depends on users: some update the same day, others run a three-month-old binary without updating. Apple's own numbers show how long that tail is. As of June 7, 2026, 79% of all iPhones were running iOS 26, nine months after release, and that is the OS update Apple pushes itself. Your app's adoption curve is slower. A crash you fixed weeks ago keeps generating support tickets from the stragglers.
OTA updates remove both delays for anything in the JavaScript layer. The fix reaches active users the next time they open the app, whether or not they ever visit the store.
That speed changes how teams operate. Hotfixes stop being emergencies that consume a release manager's day. Release confidence goes up, because a bad JS-level decision is reversible in minutes instead of days. And iteration gets cheaper: you can ship a copy change or a small UI experiment, measure it, and adjust without waiting for the next release train. Our mobile release management guide covers where OTA fits alongside your store releases and what it can't roll back.
Do app stores allow OTA updates?
Both stores permit OTA updates for interpreted code, with conditions. Apple bans downloading or installing executable code but carves out interpreted code like JavaScript, provided it doesn't change the app's primary purpose. Google Play's ban on self-updating apps doesn't apply to code running in a virtual machine.
Apple states the rule in two places. The Developer Program License Agreement, in section 3.3.1(B), prohibits apps from downloading or installing executable code, then carves out an exception for interpreted code, as long as it doesn't change the app's primary purpose, create a store or storefront for other apps, or bypass the signing, sandbox, or other security features of the OS. App Review Guideline 2.5.2 states the restriction from the review side: apps must be self-contained and may not download, install, or execute code that introduces or changes features or functionality. 2.5.2 itself carves out only educational programming apps, not interpreted code in general, so the permission React Native teams rely on comes from the license agreement rather than the review guideline. In practice, JavaScript delivered to a React Native app falls under the interpreted-code exception.
Google Play frames it through the Device and Network Abuse policy, which prohibits apps from modifying or updating themselves outside Play's own update mechanism. The policy explicitly exempts code that runs in a virtual machine or interpreter providing indirect access to Android APIs, giving JavaScript in a webview or browser as the example. That exemption is what makes OTA updates for React Native and similar frameworks allowed on Android. One condition still applies: interpreted code loaded at runtime must not enable violations of other Play policies.
The practical rule is simple. Bug fixes, UI tweaks, and content changes within your app's existing purpose are fine. New features that change what the app does, or any download of native code, are not. Cross that line and the consequences range from a rejected submission to removal from the store.
This page summarizes the policies. For the clause-by-clause breakdown, read our full policy explainer on what Apple and Google allow with OTA updates.
OTA update best practices
Good OTA discipline comes down to targeting, staging, and rollback. Version-target every update against the binary it was built for, and roll out in stages while you watch the crash-free rate. Rehearse the rollback before you need it. Teams that skip these steps meet the failure modes in production, usually as a crash loop they can't patch fast enough.
Version-target every update to the exact binary it was built against. A bundle built against one binary version can reference native modules that another version doesn't have. Skipping targeting is how you get a crash loop, so treat it as required.
Stage rollouts and watch crash-free rate before expanding. Push to 5 or 10 percent of users, hold, and check the numbers. Expanding a healthy rollout takes seconds. Recovering from a full-fleet bad push takes much longer, even with rollback.
Keep a server-side rollback ready and rehearse it. Know the exact command, know who's allowed to run it, and run the drill before an incident forces you to. Rollback discipline is a release-management habit you build through practice; our release management guide goes deeper on rollout controls.
Keep payloads small. Use diff updates where your tooling supports them. Users on slow or metered connections abandon large downloads, and an update nobody finishes downloading fixes nothing.
Never ship features that change the app's purpose over OTA. If a change would make a reviewer ask "is this still the same app?", it goes through the store.
Treat mandatory updates as an emergency tool. Forcing an immediate restart interrupts users mid-session. Reserve it for security issues and broken-beyond-use states, and let everything else apply on the next natural restart.
OTA updates vs App Store and Google Play releases
OTA updates and store releases complement each other. Native changes (new SDKs, new permissions, anything compiled) always take the store path. OTA covers the JavaScript and asset layer between releases: hotfixes, copy changes, small UI adjustments. The table below compares the three paths, because App Store and Google Play don't behave the same way once something goes wrong.
That rollback row is the real difference. Google is explicit about what halting does: "Users who already received the app version in your staged rollout version will remain on that version." Apple documents no rollback at all, and the only remedy it offers is to make the version unavailable and submit a new one. An OTA rollback is the only one of the three that reaches users who already took the bad update.
How Bitrise handles OTA updates
Bitrise ships OTA updates through CodePush, a managed service built into Release Management. You don't host it, and it isn't a standalone dashboard: releases are governed by the same users, roles, and workspaces as the rest of your apps. After App Center, including CodePush, was retired on March 31, 2025, it gives React Native and Expo teams a maintained path for the same workflow.
It shares the same open-source foundation as the original App Center service, so the protocol and the release model stay familiar: deployments, releases, staged rollouts, and rollbacks work the way App Center teams remember. Bitrise runs and develops the service itself, and maintains its own fork of the client SDK, bitrise-io/react-native-code-push, which is public and supports the React Native New Architecture and Expo.
The workflow looks like this:
- Create a CodePush deployment in Release Management (staging and production are the usual pair).
- Point your app's CodePush SDK at the deployment.
- Push updates with the Bitrise CodePush Step if you build on Bitrise CI, or through the CodePush CLI, the Release Management API, or the web UI, targeting the binary version each bundle was built against.
- Roll out gradually, watch the numbers, and roll back from Release Management if needed.
The Step reads your package.json to tell Expo and bare React Native apart, auto-detects the entry file and Hermes configuration, and exports the built package to $BITRISE_DEPLOY_DIR so a following Deploy to Bitrise.io Step picks it up.
Updates ship as deltas by default on every plan, including free. CodePush compares the bundle already on the device against the new one and sends only what changed, with measured reductions of 33% to 90% across production deployments.
Update packages can be code-signed on every plan, including free. You embed a public key in your app, the CLI signs each bundle with your private key, and the SDK rejects anything that fails verification before it installs. Signing runs through the CLI today, with web UI configuration on the way.
Release Management tracks the JS updates layered on a binary in the same place you track that binary heading through review, which makes version-targeting mistakes easier to catch.
Worth flagging one naming clash: Bitrise also has a feature called OTA app deployment. That one distributes a whole build to testers through an install page, while CodePush updates the code inside an app users already have.
Pricing starts free: the Basic plan covers up to 100,000 monthly active users, with 5,000 GiB of data transfer and 5 GiB of storage. The CodePush documentation walks through setup, and the CodePush product page covers plans and migration from App Center.
See what Bitrise can do for you
Confidently build, test, and ship high-quality mobile apps with Bitrise.
Author
Frequently Asked Questions
What can ship in an OTA update vs a store release?
OTA updates can carry JavaScript bundles, images and other assets, content, and configuration. Anything compiled into the binary, including native Swift or Kotlin code, new SDKs, and permission changes, requires a store release. The safe mental model: if it runs in the JS layer and doesn't change what the app is for, it can ship OTA.
Do OTA updates violate App Store rules?
No, not when used within Apple's rules. The interpreted-code carve-out in section 3.3.1(B) of the Developer Program License Agreement covers JavaScript delivered to a React Native app, and Review Guideline 2.5.2 is the restriction reviewers apply alongside it. The section above sets out all three conditions. What gets apps rejected is using OTA to ship executable code or change what the app is for.
How do you roll back a bad OTA update?
The rollback you control happens on the server. You publish the previous bundle again as a new release on the deployment, and apps pick up that release through the same check-for-update flow they used to get the bad one. Users don't need to do anything, and no store review is involved. Separately, the CodePush client SDK has its own automatic rollback: if an installed update crashes before the app reports it as successful, the SDK reverts that device to the previous bundle on its own. The two work together, but only the server-side action stops the bad release reaching everyone else.
Do users need to reinstall the app to get an OTA update?
No. The installed app downloads the new bundle in the background and applies it on the next restart. From the user's side nothing visible happens: there's no store visit or reinstall, and no prompt unless you choose to show one.
Do OTA updates work with Expo and Flutter?
Yes, through different tooling. Expo has its own OTA mechanism, expo-updates (the library behind EAS Update), and Expo apps also work with CodePush-style updates: the Bitrise-maintained React Native SDK supports both Expo and the React Native New Architecture. Flutter compiles to native code, so it can't swap JavaScript bundles, but patch tooling such as Shorebird gets you the same outcome, a code change that ships without a store release: it sends a patch of your Dart code and runs it through the Dart VM or a Dart interpreter, which is how it stays inside the store rules. Fully native apps have no OTA path for code.

