A React Native application is not one artifact.
The binary users install from the App Store or Play Store contains native code, native dependencies, platform configuration, a JavaScript engine, and an embedded JavaScript bundle.
An OTA release replaces only part of that system: the JavaScript bundle and compatible bundled assets.
That creates the most important safety question in any React Native OTA system:
Can this new JavaScript safely execute inside the native binary already installed on the device?
A runtime version exists to answer that question.
In Bundle Drop, runtimeVersion names the native compatibility boundary. An OTA bundle is only eligible when its platform and runtime version match the installed binary. It is not a “newer than” comparison and it is not simply another app version.
App version and runtime version solve different problems
It is tempting to reuse the application version for everything.
For example:
App version: 4.2.0
Runtime version: 4.2.0
That may be convenient for a particular release strategy, but the two concepts are different.
The application version answers something like:
Which product release is this?
The runtime version answers:
Which native interface can this JavaScript safely run against?
Multiple JavaScript releases can therefore exist inside the same native runtime.
And two native app releases can sometimes share a runtime if their native compatibility surface is unchanged.
Bundle Drop records both kinds of identity separately; its CI result metadata, for example, includes the application version and runtimeVersion as distinct fields.
That separation matters because otherwise the OTA compatibility model becomes coupled to product-versioning decisions that may have nothing to do with native safety.
Keep the runtime when the native boundary did not change
Suppose your current configuration looks like this:
module.exports = {
runtimeVersion: {
ios: "4.2-native-1",
android: "4.2-native-1",
},
};
You fix some TypeScript logic.
Or change copy.
Or adjust styling.
Or add a screen built entirely from native capabilities that already exist in the installed binary.
Those changes do not necessarily create a new compatibility boundary.
The existing binaries already contain what the new JavaScript needs.
So the runtime can remain unchanged.
Bundle Drop’s current public model explicitly recommends keeping a platform’s runtime literal stable for OTA-compatible JavaScript and asset changes.
Bump the runtime when JavaScript can depend on new native state
Now imagine adding a new iOS native module.
The next JavaScript release imports that module.
Older iOS binaries cannot safely run the new bundle because the native module simply does not exist inside them.
Changing the OTA runtime label does not magically install the module.
The correct sequence is:
- change the native code;
- create a new iOS runtime line;
- build and distribute a new iOS binary;
- publish OTA updates that depend on the new native capability only to that runtime.
The old binary continues on its previous compatibility line.
This same logic applies to changes such as native dependencies, permissions, generated native configuration, JavaScript-engine changes, architecture changes, or Expo config-plugin output that materially changes the native projects.
A runtime version is a gate, not an upgrade mechanism.
iOS and Android do not have to move together
This is one of the most useful properties of a per-platform runtime model.
Suppose you add a native dependency only on iOS.
You might change:
runtimeVersion: {
ios: "4.2-native-1",
android: "4.2-native-1",
}
to:
runtimeVersion: {
ios: "4.2-native-2",
android: "4.2-native-1",
}
Android has not gained or lost any native capability.
There is no compatibility reason to force it onto a new runtime line.
Bundle Drop’s canonical runtime article explicitly models these as independent lanes: an iOS native change can advance iOS while Android continues resolving the previous Android runtime.
This is more than a configuration detail.
React Native teams frequently ship platform-specific native changes.
Forcing both platforms to advance every time one platform changes creates unnecessary runtime fragmentation and release bookkeeping.
Old and new binaries normally coexist
Publishing a new store binary does not mean every user installs it immediately.
Some users update quickly.
Some wait days or weeks.
Managed devices can lag even longer.
During that period, production can legitimately contain:
Older iOS binary
runtimeVersion = ios:4.2-native-1
Newer iOS binary
runtimeVersion = ios:4.2-native-2
Both binaries may continue checking the same production channel.
The important part is that each installation resolves only updates that belong to its own runtime line.
Bundle Drop’s public resource explicitly describes channels containing several runtime lines while installed binaries receive only the exact compatible platform/runtime combination.
This lets teams continue maintaining a safe OTA path for users who have not yet adopted the newest store release.
Runtime version and channel are different axes
Another common mistake is treating channels as compatibility boundaries.
They are not.
A channel answers:
Which release track should this app follow?
Examples might be:
develop
beta
production
The runtime answers:
Which native binaries can execute this update?
A production channel can therefore contain releases for multiple runtime lines.
Likewise, two binaries on the same runtime could follow different channels.
Bundle Drop documents these concepts separately because collapsing them creates awkward release models such as:
production-ios-v4-native2
where release track, platform and compatibility are all encoded into one string.
Keeping the axes independent produces a cleaner model:
Channel: production
Platform: ios
Runtime: 4.2-native-2
Each value answers one question.
A runtime bump should follow a native change, not compensate for uncertainty
It can be tempting to bump the runtime frequently “just to be safe.”
That is safe in one narrow sense: fewer binaries will be considered compatible.
But unnecessary runtime churn creates operational cost.
Every new runtime line creates another compatibility branch that may coexist in production.
Too coarse a boundary risks sending JavaScript to an incompatible binary.
Too fine a boundary creates unnecessary fragmentation.
The useful rule is:
Change the runtime when the native compatibility surface changed.
Do not use it merely as a release counter.
Expo introduces an authority question
Expo projects have another concept called runtimeVersion.
That means an OTA integration needs to be explicit about which value is authoritative rather than silently mixing two independent runtime calculations.
Bundle Drop’s default Expo setup uses the literal per-platform runtime values from bundle.drop.config.js, with those values embedded into the native build and used by uploads.
For advanced Expo workflows, the current public SDK also supports opting into Expo runtime authority. In that mode, Bundle Drop requires the runtime identity from the corresponding native build rather than guessing it during OTA publication; CI documentation describes using a matching local or EAS build receipt for that path.
The broader principle is useful even outside Bundle Drop:
one component should be authoritative for native compatibility.
If the build system and OTA publisher derive that boundary differently, you have created exactly the ambiguity the runtime version was meant to remove.
Runtime compatibility should be decided at release time
You do not want to discover a compatibility mistake because a production device crashed.
A useful release review asks:
- Did native code change?
- Did a native dependency change?
- Did permissions or generated native configuration change?
- Did the JavaScript engine or architecture change?
- Did an Expo config-plugin change alter native output?
- Did either platform change independently?
If the answer is yes, review that platform’s runtime before publishing the JavaScript release.
Bundle Drop’s setup and troubleshooting docs also surface runtime mismatches as a first-class delivery condition: an installation does not receive a bundle from a different runtime line.
That fail-closed behavior is important.
“No update available” is much safer than “run JavaScript against a native interface it was never built for.”
Runtime version is not enough by itself
Compatibility only answers whether a bundle can run.
It does not answer:
- whether this is the correct release channel;
- whether the installation belongs in a targeting cohort;
- whether the staged-rollout percentage includes it;
- whether the bundle passed integrity verification;
- whether a previously installed update is healthy.
Those concerns need their own controls.
This is why production OTA systems usually have several independent dimensions:
Platform
Runtime compatibility
Channel
Targeting
Rollout state
Release identity
Health / recovery state
Runtime version is one of them, but it is arguably the first one.
If the native compatibility decision is wrong, the rest of the release controls cannot make the JavaScript safe.
A useful mental model
Think of a runtime version as a native contract identifier.
The installed binary says:
I provide native contract
ios:4.2-native-1.
An OTA bundle says:
I require native contract
ios:4.2-native-1.
If the contracts match, the release can continue through the rest of the eligibility process.
If they do not, the OTA bundle is not a candidate for that binary.
That model remains understandable whether the app has one store version or five active versions, whether iOS and Android move together or separately, and whether the release arrives through a development, beta, or production channel.
That is exactly what a good compatibility boundary should do.
The release rule to keep
OTA lets React Native teams move compatible JavaScript and assets independently from store binaries.
That independence only remains safe when the OTA system respects the native application underneath it.
Keep the runtime when the installed binaries already provide everything the update needs.
Create a new runtime line when the native interface changes.
Move only the platforms that actually changed.
And allow old and new runtime lines to coexist while users adopt the new store binary at their own pace.
Bundle Drop implements that model with literal per-platform runtime versions by default, plus an explicit Expo-authority option for teams that need it. The principle itself is provider-independent:
OTA compatibility should be explicit, stable and easy to explain during an incident.
Top comments (0)