RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-01Intermediate

EAS Update Published but Nothing Changes? Five Patterns That Quietly Break OTA Delivery in Rork

You ran eas update, the CLI showed a green Published, but your iPhone keeps loading the old code. Here are the five patterns I keep running into, plus a five-minute diagnostic flow you can use the next time OTA goes silent.

Rork515EAS Update6OTA6Expo149Troubleshooting38iOS109Android43

The whole point of EAS Update — shipping fixes without resubmitting to the App Store — is fantastic when it works, and exhausting when it does not. The frustrating thing is that EAS Update tends to fail silently: the CLI prints a cheerful "Published" line, Sentry stays quiet, and yet your test device keeps running last week's bundle.

I have spent more late-night hours than I care to admit on this, and after enough rounds I have learned that the failures are almost never random. They cluster into about five recurring patterns, all caused by a small mismatch between what you published and what the installed app is actually allowed to receive. This article walks through those five patterns in the order I check them, and ends with a five-minute diagnostic flow you can run on a tired Friday evening.

Step Zero: Confirm Which Channel You Are Actually Publishing To

Roughly nine out of ten OTA disasters come down to the channel you published to does not match the channel the installed binary is listening to. So before anything else, line those two up.

# Show the channels that exist in your project
eas channel:list
 
# Show recent updates on a specific branch
eas update:list --branch production

Your eas.json likely looks something like this:

{
  "build": {
    "production": {
      "channel": "production",
      "autoIncrement": true
    },
    "preview": {
      "channel": "preview",
      "distribution": "internal"
    }
  }
}

If your TestFlight build came from the production profile but you ran eas update --branch preview, the production binary will never see that update. Your binary only listens to the channel it was compiled with. Decide explicitly: am I testing on a preview build or a production build today? Almost every "it does not work" report I get answers itself once that question is answered.

Pattern 1: Mismatched Runtime Version

This is the one I trip over most often.

EAS Update is designed around the idea that JavaScript-only changes can be hot-shipped, but anything that touches native code requires a full rebuild. To enforce that, every published update is tagged with a runtimeVersion, and the installed app will only accept updates whose runtime version exactly matches its own. If they differ — even by a single digit — the device silently ignores your publish.

Check your strategy in app.json:

{
  "expo": {
    "runtimeVersion": {
      "policy": "appVersion"
    }
  }
}

With the appVersion policy, every change to expo.version produces a new runtime version. So if version 1.2.0 is live in the App Store and you bumped your local app.json to 1.3.0 before running eas update, the 1.2.0 users in the wild are not eligible for that update. They will keep running 1.2.0 forever.

The fix is unglamorous but reliable: keep your local app.json version pinned to whatever is currently in the store while you publish OTA fixes. Bump it only when you are about to ship a new binary.

Pattern 2: Misconfigured expo-updates on the Client

Three things have to line up at build time: the updates.url, the runtimeVersion strategy, and the channel ID baked into the iOS / Android binary. Rork-generated projects have these wired up correctly out of the box, but it is easy to bend something while debugging and forget to undo it.

{
  "expo": {
    "updates": {
      "url": "https://u.expo.dev/your-project-id",
      "enabled": true,
      "checkAutomatically": "ON_LOAD",
      "fallbackToCacheTimeout": 0
    },
    "runtimeVersion": { "policy": "appVersion" }
  }
}

The two settings I see go wrong most often: enabled: false (sometimes set during a debug session and forgotten), and checkAutomatically: ON_ERROR_RECOVERY (which means the app only checks for updates after a crash). I once shipped a production build with enabled: false and lost two days to it before noticing.

Pattern 3: Users Did Not Restart the App Twice

This one is by spec, but it surprises almost everyone the first time.

The default behavior of expo-updates is: on launch, check for a new bundle in the background; if found, download it and apply it on the next launch. So if your tester is currently running the app, your publish does nothing for them right now. They have to fully quit the app — not background it, actually swipe it away — and reopen it. Twice.

If you cannot reproduce on your own device, do this routine first:

  1. Swipe the app away from the multitasker
  2. Open it once (this triggers the download)
  3. Swipe it away again
  4. Open it again (this is when the new bundle actually runs)

If you need updates to apply immediately for a critical fix, do the check-and-reload yourself in code:

import * as Updates from "expo-updates";
import { useEffect } from "react";
import { Alert } from "react-native";
 
export function useImmediateUpdate() {
  useEffect(() => {
    if (__DEV__) return; // skip in dev/Expo Go
 
    (async () => {
      try {
        const update = await Updates.checkForUpdateAsync();
        if (update.isAvailable) {
          await Updates.fetchUpdateAsync();
          Alert.alert(
            "Update available",
            "The app will reload to apply the update.",
            [{ text: "OK", onPress: () => Updates.reloadAsync() }]
          );
        }
      } catch (e) {
        // Network errors are fine — the next launch will retry.
        console.warn("[updates]", e);
      }
    })();
  }, []);
}

The expected behavior: right after you publish, when the user reopens the app, they see a one-tap dialog and the bundle hot-swaps. Do not skip the __DEV__ guard. Without it, you will get nonstop "no updates available" errors during local development against Rork or Expo Go.

Pattern 4: You Are Publishing to the Wrong Build Profile

Updates published with eas update --branch development reach development builds only. Updates on the preview branch reach internal-distribution preview builds only. Obvious in writing, but it bites people who hand around TestFlight builds compiled from a production profile and then publish fixes to development.

Cross-check the build profile of your test device:

eas build:list --platform ios --limit 5

The profile column in the output should match the branch name you pass to eas update --branch <name>. If you are not sure which TestFlight build is from which profile, this list is the source of truth.

Pattern 5: Stale Cached Bundle on the Device

If everything above checks out and updates still are not landing, it is time for the nuclear option.

expo-updates boots from a cached bundle while it fetches the next one in the background. Under certain conditions — corrupted downloads, abrupt force-quits during a fetch — the cache can hold onto an old bundle in a way that no amount of reloadAsync will dislodge.

The clean diagnostic step is to uninstall the app and reinstall it from TestFlight or the Store. That returns the device to "embedded bundle only" — exactly what a fresh user has on day one.

  • Reinstall and the new content shows up → you had a client-side cache issue. The fix usually arrives the next time the app is opened normally.
  • Reinstall and you still see the old code → your publish never reached this binary. Loop back to patterns 1 through 4.

A Five-Minute Diagnostic Flow

When you are panicking, here is the shortest path to a real diagnosis:

  1. eas channel:list to confirm where you published
  2. eas update:list --branch <name> to confirm the latest publish is registered there
  3. Note the runtimeVersion of that publish
  4. Check the runtime version your test app is actually running
  5. Mismatch → revert your local app.json version to the live store value and republish
  6. Match → fully quit the app, open it twice, see if it lands
  7. Still no → uninstall, reinstall from store, retry

The fastest way to read the device's own state is to expose it inside the app. I keep a tiny debug component on a hidden screen in every project:

import * as Updates from "expo-updates";
import { Text, View } from "react-native";
 
export function UpdateDebugInfo() {
  return (
    <View style={{ padding: 16 }}>
      <Text>Runtime: {Updates.runtimeVersion}</Text>
      <Text>Channel: {Updates.channel}</Text>
      <Text>Update ID: {Updates.updateId ?? "(embedded)"}</Text>
      <Text>Created: {Updates.createdAt?.toISOString() ?? "-"}</Text>
    </View>
  );
}

Four lines and you instantly know which bundle the user is on. In production, hide it behind a long-press gesture in the settings screen — that has saved me real money in support time.

A Note on Why These Failures Look Identical

Something I find worth saying out loud: the five patterns above all look identical from the user's seat. The user opens the app, sees old behavior, and reports "the update did not work". You have no way to distinguish a runtime mismatch from a stale cache from a wrong-channel publish without digging into the device's own update state. That is exactly why the UpdateDebugInfo component is so useful. The cost of building it is fifteen minutes; the cost of not having it, the first time a paying customer reports a stuck build, is significantly more.

The deeper lesson — and the one that took me longest to internalize — is that OTA reliability is mostly an observability problem, not a coding problem. The CLI tells you what you published. The user's device knows what it actually downloaded. Until you wire those two ends together, every reported "stuck" is going to be a guessing game.

Related Reading

If you want to step back and understand the whole OTA story rather than just the failure modes, start with the Rork EAS Update / OTA delivery guide. Once you have publishing under control, the natural next step is automating it via CI: Building a CI/CD pipeline with GitHub Actions and EAS Build. And if your problem is not "OTA does not land" but "the build itself does not even compile", jump over to Patterns for fixing React Native build errors in Rork first — there is no point publishing OTA on a binary that cannot be built.

What To Do Tonight

OTA failures are almost always configuration mismatches, not code bugs. So before you start firing off eas update in panic, slow down for sixty seconds and run eas channel:list followed by eas update:list --branch <name>. Most of the time, that single check tells you exactly which lever is misaligned.

The one concrete thing I would do tonight: drop that UpdateDebugInfo component into a hidden screen of your current project. The next time someone reports "my version is stuck", you will be able to read their actual runtime, channel, and update ID in seconds — and that single piece of information collapses most of these five patterns down to a one-line answer.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-25
Why Your Rork App Icon Won't Update — Cache Fixes for iOS and Android
Replaced your Rork app icon but still seeing the old one? Walk through the layered causes — iOS SpringBoard cache, EAS Build cache, missing Android adaptive icons — and the exact fixes that get the new icon to stick.
Dev Tools2026-06-28
Ship EAS Updates to a Few First, and Halt Automatically on Crash Rate
Because OTA updates reach everyone instantly, a bad update reaches everyone instantly too. Here is a three-layer design: ship EAS Update to a small canary, decide expand-or-halt from crash-free rate automatically, and hold a safety net on the device — with working code.
Dev Tools2026-06-24
When EAS Update Ships but the Bug Won't Die — Why OTA Stalls Silently, and How I Operate Around It
EAS Update can succeed and still fail to reach a slice of your users. These are field notes on runtimeVersion drift, updates that publish but never get adopted, and choosing the right rollback — with the instrumentation that actually helped on my Rork apps.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →