EAS Update OTA Strategy in 2026: Channels, Rollouts, and Rollbacks Without Breaking Review
OTA updates are the best superpower Expo gives you — and the easiest way to get your app pulled. Here's how we structure EAS Update channels, staged rollouts, and rollbacks without tripping Apple's review rules.

OTA updates are the single best reason a lot of teams choose Expo over going fully native. They're also the fastest way to get a stern email from App Review or, worse, push a bad JS bundle to 100% of users at 5pm on a Friday. After shipping EAS Update across a dozen production apps, we've settled on a setup that's boring on purpose.
This is that setup — channels, rollout strategy, rollback playbook, and the App Store rules that quietly kill OTA pipelines.
What EAS Update Actually Ships (And What It Can't)
EAS Update ships your JavaScript bundle and bundled assets. It does not ship native code. If you add a new native module, change an Expo SDK version, or touch anything in ios/ or android/, you're building a new binary and going through the stores.
That boundary matters because Apple's guideline 3.3.1 (formerly 3.2.2) is explicit: you can update interpreted code (JS), but you can't materially change the app's purpose or add features that would have required review. In practice this means:
- Fixing bugs over the air: fine.
- Tweaking copy, layout, feature flags: fine.
- Shipping a whole new feature you hid behind a remote flag and turning it on next day: technically against the rules, and we've seen apps flagged for it.
Google is much more relaxed, but a bad OTA on Android still ruins your week.
Channel Design: Stop Using production for Everything
The default temptation is to have one channel per environment: development, preview, production. That falls apart the moment you need to ship a hotfix to v2.4.0 users while v2.5.0 is in review.
We pin channels to release trains, not environments:
// eas.json
{
"build": {
"production-2-5": {
"channel": "production-2-5",
"distribution": "store",
"env": { "APP_ENV": "production" }
},
"production-2-4": {
"channel": "production-2-4",
"distribution": "store"
},
"preview": {
"channel": "preview",
"distribution": "internal"
}
}
}
Each minor version gets its own channel. A binary built for production-2-5 will only ever receive updates published to that channel. That means:
- You can patch v2.4.x users who haven't upgraded.
- A bad bundle published to 2.5 can't accidentally reach 2.4 users running an older runtime.
- Your
runtimeVersionpolicy (we useappVersionor afingerprint-style policy depending on the project) does the heavy lifting underneath.
Runtime Version: Pick One Policy and Stick to It
// app.config.ts (excerpt)
{
"runtimeVersion": { "policy": "appVersion" },
"updates": {
"url": "https://u.expo.dev/<your-project-id>",
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 0
}
}
appVersion is the easiest to reason about: 2.5.0 and 2.5.1 share a runtime, 2.6.0 doesn't. If you're doing more aggressive native module changes between patch versions, switch to the fingerprint policy so EAS computes runtime compatibility from your native code surface. Mixing policies across builds is how you end up serving updates to binaries that crash on boot.
Staged Rollouts: The 1 / 10 / 50 / 100 Cadence
EAS Update supports rollout percentages on a published update. We never go straight to 100%. The cadence we use on most projects:
- 1% for 1 hour. Catches the obvious — boot loops, crash-on-launch from a missing asset, a misconfigured env var.
- 10% for 4–8 hours. Wide enough to surface crashes that depend on specific OS versions or locales. Watch Sentry, watch your crash-free sessions metric.
- 50% overnight. Gives you a full traffic profile across timezones.
- 100%.
Publishing with rollout looks like this:
# Publish to channel at 0% — nobody gets it yet
eas update --channel production-2-5 \
--message "fix: cart total rounding" \
--rollout-percentage 0
# Then ramp it
eas update:edit --rollout-percentage 1
eas update:edit --rollout-percentage 10
eas update:edit --rollout-percentage 100
In our experience, roughly 70–80% of crash regressions we've caught from OTA bundles showed up inside the first hour at 1%. The rest tend to be locale, low-end Android, or a specific OS version. Don't skip the overnight 50% step on apps with heavy international traffic.
Always Have a Health Gate
A percentage isn't a strategy — it's a knob. The strategy is the gate. We wire a small check that pauses the ramp if:
- Crash-free sessions drop more than ~0.3 percentage points vs the previous bundle.
- A new error type appears in Sentry above some absolute count.
- Login or checkout success rate drops noticeably in product analytics.
None of those are magic numbers — calibrate to your app's baseline. The point is that the human deciding to push from 10% to 50% should be looking at dashboards, not a calendar.
Rollback: Republish, Don't Delete
The single biggest mistake we see teams make: they try to "delete" a bad update. EAS Update is content-addressed and versioned. The correct rollback is to republish the previous good bundle to the same channel.
# Find the last known good update on this branch
eas update:list --branch production-2-5
# Republish it — this creates a new update that points at the same bundle
eas update:republish --group <good-update-group-id> \
--message "rollback: revert cart rounding fix"
Why not just roll back the rollout to 0%? Because users who already received the bad bundle have it cached locally. They won't pick up a new one until you publish something newer than what they have. Republishing the old bundle as a fresh update is what gets them back to safety.
Keep a runbook with these commands pinned in your team chat. When something is on fire at 11pm, nobody wants to be reading docs.
The App Review Pitfalls Nobody Warns You About
A few that have bitten us or clients we've cleaned up after:
- Feature flags that flip immediately after review. If a reviewer sees a stripped-down app and you light up payments or social features the next day, you're on borrowed time. Build the full feature, gate it on a server flag, and turn it on before you submit if you want it visible during review.
- OTA-only "new" screens. Adding a brand new tab via JS-only update is the kind of thing that gets cited. New surfaces should ride a binary release.
- Background behavior changes. Anything that affects how you use location, notifications, or background tasks needs to match what you declared in the binary's Info.plist or Android manifest. You can't OTA your way into new permissions.
- Forgetting
expo-updatesfallback behavior. If your update server is unreachable on cold start, the default is to launch the cached bundle. Good. But if you setfallbackToCacheTimeouttoo high, you'll see startup time regressions on flaky networks. Keep it at 0 unless you have a specific reason.
Google Play has a separate but related rule: they want the app's behavior on the Play Store listing to match the app users get. Same principle — don't use OTA to change what the app fundamentally is.
Native vs OTA: When We Pick Swift/Kotlin Instead
We like Expo, but OTA isn't a free lunch. On a recent project with very heavy native camera and ML work, we ended up writing the capture pipeline in Swift and Kotlin and only kept the surrounding UI in React Native. OTA updates still ship the UI fixes fast; the native modules ride binary releases.
If your app is 80% JS-rendered UI over an API, EAS Update will save you weeks per year. If it's a thin JS shell over heavy native work, you're going to be cutting binaries often anyway, and the OTA story matters less. Be honest about which one you have before you build your release ops around it.
Where We'd Start
If you're setting this up from scratch on a real product:
- Move to per-release-train channels (
production-2-5, etc.) and pick aruntimeVersionpolicy you'll actually stick with. - Wire
eas update --rollout-percentageinto your deploy pipeline, defaulting to 0% on publish. - Write the rollback runbook — three commands, pinned in chat — before you need it.
- Decide, in writing, what your team will and won't ship via OTA. Share it with whoever talks to App Review.
Do that and OTA goes from a foot-gun back to what it's supposed to be: the fastest, safest way to fix a mobile bug in production. If you'd like a hand designing the release ops for your app, our mobile team does this work for a living.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Biometric Auth in React Native 2026: Face ID, Passkeys, and the Keychain Traps
Face ID and fingerprint look simple from the product side. The keychain semantics, passkey fallbacks, and review-time edge cases are where React Native teams lose a week. Here's what we've learned shipping it.

Push Notifications in Expo and React Native: Why Yours Are Silently Failing in 2026
Push delivery looks fine in your dashboard but users swear they never got the notification. Here's what actually breaks push on iOS and Android in 2026, and how to debug it before support tickets pile up.

In-App Purchases in React Native: What Apple and Google Actually Reject in 2026
A practical breakdown of the IAP rejection patterns we keep seeing in React Native and Expo apps in 2026 — from receipt validation to subscription restore flows — with code you can lift straight into your project.
