skip to content

Runtime Version Policies

The runtimeVersion field ties an update to the native code it was built for, set by hand or by the appVersion, nativeVersion or fingerprint policy. Interviewers ask how a mismatched update crashes.

part ofExpo (React Native)overview, primer and where to startread it →
on this pageshow

explore

questions

4

In an Expo app shipping EAS Update, what does runtimeVersion guarantee, and which changes force a new runtime version and a new build?

level: juniorimportance: must knowfreq 44%

answer

  1. native layer vs update layer
  2. a label baked into the binary
  3. exact match, per platform
  4. native module, config plugin, SDK upgrade

basics

~20 s

runtimeVersion labels the native layer a build contains; EAS Update serves an update only to builds whose runtime version and platform match exactly. Any change to native code or native config needs a new build and a new runtime version.

solid answer

~50 s

A build has two layers: the native binary (React Native, every library with native code, permissions, icon, splash) and a swappable update layer (the JS bundle and its assets). `runtimeVersion` names the native layer. It is written into the binary at build time, `eas update` resolves it again from the project when publishing and stamps it on the update, and EAS serves an update only to builds of the same platform whose runtime version matches **exactly**. So an update may change JavaScript and assets, but adding, removing or upgrading a library with native code, changing a config plugin or native config in `app.json`, or upgrading the Expo SDK needs a new build with a new runtime version. Keeping the old runtime version across such a change is how an update ends up calling native code the build does not have.

code

json · 9 lines
json
{
  "expo": {
    "version": "1.4.0",
    "runtimeVersion": "1.4.0",
    "updates": {
      "url": "https://u.expo.dev/<project-id>"
    }
  }
}

go deeper

for a junior

Recall the two layers and the rule: updates carry JavaScript and assets, and anything native needs a new build with a new runtime version.

for a middle

Explain where the value lives (baked into the binary, re-resolved at publish), that matching is exact and per platform, and list which changes count as native.

for a senior

Show how a stale label turns into a launch crash, and describe the logging and checks you would put in place so a native change can never share a runtime with old builds.

for a principal

Frame runtimeVersion as the contract between release engineering and feature teams, and decide who owns bumping it and which changes the organisation allows over the air.

## Two layers in every Expo build An app that uses **EAS Update** (the hosted update service) together with the **`expo-updates`** library (the client that runs inside the app) is built as two layers: - **The native layer** is the compiled binary: React Native itself, the Hermes engine, every library that ships Kotlin, Swift or Objective-C code, and native configuration such as permission strings in `Info.plist` and `AndroidManifest.xml`, the app icon and the splash screen. It only changes when a user installs a new build. - **The update layer** is the JavaScript bundle Metro produces plus the images, fonts and other assets that JavaScript loads. This is the part an update replaces. JavaScript reaches the native layer through native modules and native views. If the bundle expects a module, a method or a view that the binary does not contain, it fails at runtime. An update layer is therefore only safe on a binary whose native layer it was written against. ## What `runtimeVersion` is **`runtimeVersion`** is a string that names one particular native layer. You set it in the app config (`app.json` or `app.config.ts`) either as a plain string such as `"1.0.0"` or as a policy object such as `{ "policy": "appVersion" }` that derives the string from something already in the project. Two moments matter: 1. **At build time** the resolved value is written into the binary: the `EXUpdatesRuntimeVersion` key in `Expo.plist` on iOS and the `expo.modules.updates.EXPO_RUNTIME_VERSION` meta-data on Android (with the fingerprint policy, a reserved placeholder plus a bundled hash file). From then on it is fixed for that install, and `Updates.runtimeVersion` from `expo-updates` reads it back at runtime. 2. **At publish time** `eas update` resolves the runtime version again, per platform, from the project as it is at that moment, and stamps that value on the update. The label is only as honest as the project state it is computed from. Nothing compares the two moments for you unless you choose a policy or a CI check that does. ## How EAS decides who receives an update Every update request from the app carries its runtime version (sent as the `Expo-Runtime-Version` header) together with its platform and channel. EAS Update then applies its update policy: - the update's platform must equal the build's platform; - the update's runtime version must equal the build's runtime version **exactly**: no semver range, no "closest compatible" fallback; - the build's channel decides which branch it reads from, which is a separate subject from compatibility. A build therefore only ever sees updates published for its own native layer, and an update stamped with a runtime version that no installed build reports is simply never downloaded. ## What can and cannot go over the air | Change | Ships in an update? | Why | |---|---|---| | Bug fix in a screen, hook or state store | Yes | It lives in the JS bundle | | New image or JSON file loaded by JavaScript | Yes | Assets travel with the update | | Upgrading a pure-JavaScript dependency | Usually | Metro bundles its code, unless it now needs a newer native peer | | Adding, removing or upgrading a library with native code | No | The binary lacks the native half | | Changing a config plugin, permission string, icon or splash | No | These are written into the native projects at build time | | Upgrading the Expo SDK or React Native | No | The whole native layer changes | The rule behind the table is short: **a native change means a new build, and the new build needs a new runtime version.** Only then do you publish updates that rely on the change, targeted at that new runtime. ## When the rule is broken Nothing in EAS inspects the bundle's imports. If a team adds a native library and publishes without changing the runtime version, the update carries the old label, every existing build on that runtime accepts it, and the first import of the new module throws; for an Expo module the error reads `Cannot find native module '...'`. That is why the choice of how the label is computed (by hand or by a policy) matters so much, and why runtime versions are worth logging next to `Updates.updateId` in crash reports. ## Key points - `runtimeVersion` is a compatibility label for native code, not a marketing version, although the `appVersion` policy happens to copy the app version. - Matching is exact and per platform; iOS and Android builds may carry different runtime versions. - An update can change JavaScript and the assets it loads, never native code or native configuration. - Bumping the runtime version is the developer's contract, whether done by hand or delegated to a policy.

  • How can the running app report which runtime version it was built with?
    `expo-updates` exports `Updates.runtimeVersion`, the value baked into the current binary (or `null` when none is configured), and `Updates.updateId` for the update that is running. Logging both with crash reports tells you which native layer and which update a failing session had, which is the first thing you need when an update misbehaves on some builds only.
  • Does upgrading a JavaScript-only dependency require a new runtime version?
    Not by itself. Metro bundles a pure-JavaScript package into the update layer, so it can ship over the air. The exception is an upgrade that now expects a newer native peer, for example a UI library that requires a newer version of a native gesture or animation module; then the native layer changes and you need a new build and runtime version.
  • Can iOS and Android builds of the same release have different runtime versions?
    Yes. The runtime version is resolved per platform, and `ios.runtimeVersion` or `android.runtimeVersion` can override the top-level value. `eas update` publishes one update per platform, each stamped with that platform's runtime version, and EAS matches platform and runtime version independently.

A runtime version is like the socket type printed on a lamp fitting: the shop happily sells you any bulb with the same printed type, and never tests whether it actually fits. If the fitting changed but the label did not, the bulb arrives and fails in the socket.

saying these in an interview costs you the question

  • An update can add a native module as long as the JavaScript imports it.
  • runtimeVersion is just the store version number under another name.
  • EAS serves the closest compatible runtime version when there is no exact match.
  • Changing a permission string in app.json can ship in an update.
  • One runtime version always covers both iOS and Android builds.
open as a page

In an Expo app config, how do the appVersion, nativeVersion and fingerprint runtimeVersion policies differ, and when would you set a plain string instead?

level: middleimportance: should knowfreq 30%

basics

~10 s

appVersion copies the app version, nativeVersion adds the build number, and fingerprint hashes native-affecting project inputs. A plain string gives full manual control and, besides fingerprint, is the only option in a bare project.

open as a page

An Expo app on the appVersion runtimeVersion policy adds a native camera module, the team publishes an EAS Update without bumping version, and 1.4.0 users crash at launch: what went wrong, and how do you prevent it?

level: seniorimportance: should knowfreq 28%

basics

~20 s

appVersion still resolved to 1.4.0, so EAS served the update to 1.4.0 binaries that lack the camera's native code, and its import threw at launch. Ship native changes in a new build with a new runtime version.

open as a page

With Expo's fingerprint runtimeVersion policy, what does @expo/fingerprint hash, and why can the runtime version change unexpectedly or fail to change when it should?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

@expo/fingerprint hashes, per platform, the app config, config plugins, autolinked native packages and any committed native folders, not app JavaScript. Version bumps or dynamic config change it unexpectedly; edits inside inline plugin mods and ignored paths escape it.

open as a page