Managing Updates in Expo with OTA
Practical notes on shipping quick fixes without waiting for the app store review cycle, using Expo Updates — including its real limits.
One of the most frustrating constraints in mobile development is the store review cycle. You spot a bug, fix it, cut a build — and then you sit there waiting for the App Store review to finish before the fix ever reaches your users. For a critical bug, that wait is simply unacceptable.
OTA (Over-the-Air) updates solve this problem, but using them without fully understanding them creates its own problems. I’ve been running Expo Updates in production for a few years now; here’s what I’ve learned.
What OTA is — and what it isn’t
An OTA update means pushing the app’s JavaScript bundle and assets to devices without going through a store review. Expo’s OTA infrastructure handles this via the expo-updates package.
The critical boundary: native code cannot be changed over the air. When you add a new native module, bump an Expo SDK version, or change app permissions, a store release is mandatory. OTA only carries changes in the JavaScript layer.
Keeping that distinction sharp in your head matters. If a change you planned to ship via OTA turns out to require a native dependency, you’ll have to rethink the whole plan from scratch.
Basic configuration with Expo Updates
I define update channels in eas.json:
{
"build": {
"production": {
"channel": "production"
},
"staging": {
"channel": "staging"
}
}
}
And I set the update policy in app.json or app.config.js:
{
"expo": {
"updates": {
"enabled": true,
"fallbackToCacheTimeout": 0,
"checkAutomatically": "ON_LOAD"
}
}
}
With fallbackToCacheTimeout: 0, if an update is available on launch the app downloads it immediately and applies it on the next open. This “download now, apply next session” approach gives the least-friction experience.
Publishing an update with EAS Update
# Send an update to the staging channel
eas update --channel staging --message "Form doğrulama hatası düzeltildi"
# Push to production
eas update --channel production --message "v2.4.1 hotfix"
This command uploads the JavaScript bundle and any changed assets to EAS (Expo Application Services) servers and links them to the target channel. Devices listening to that channel will receive the update the next time the app is opened.
Update strategy
In production I work with two approaches: passive updates and force updates.
A passive update means the app downloads the update in the background and applies it on the next launch. The user notices nothing. This is sufficient for non-critical fixes.
For a critical security vulnerability or a serious functional bug, you may need to force the update through. You can take control via the expo-updates API:
import * as Updates from 'expo-updates';
async function checkForUpdate() {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
} catch (e) {
// Silently skip if there's no network
}
}
You can call this function on app launch or when the user navigates to a specific screen. reloadAsync() restarts the app with the updated bundle.
Channel management and the testing workflow
Testing on the staging channel before pushing any OTA to production is a non-negotiable part of my workflow. Because EAS Update runs the same command against different channels, this is trivially simple.
Before publishing any update I ask myself one question: does this change touch a native dependency? If no, OTA is fine. If yes, I switch to the store release plan.
The real value of OTA
Every release I send to the store has to wait for Apple’s review. That window varies, but it’s typically 24–48 hours. Holding a production bug back for that long is usually unacceptable.
With OTA updates I can get JavaScript-layer fixes to every user within minutes. That speed significantly alleviates one of mobile development’s biggest constraints. But no matter how powerful the tool is, you have to stay conscious of the native boundary — otherwise you’ll find yourself trying to solve the wrong problem with the wrong tool.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.