One of my apps had been sitting with a slow-startup review for a while. When I heard that Expo had shipped a patch fixing a startup-time regression, I opened package.json expecting a quick win.
It said "expo": "~57.0.9". A tilde, so patches come along automatically — that was my assumption. I rebuilt without changing anything.
Nothing improved. The build was still on 57.0.9, nine patches behind.
I knew the range syntax well enough to recite it, and had never once checked what it actually resolves to in a project that was already running. As an indie developer juggling several apps at once, an assumption like that can sit undisturbed for months. This is what I found when I ran the checks properly.
Nine patches in thirty days
Before anything else, it helps to know how fast patches arrive. The npm registry stores publish timestamps, so you can ask it directly.
npm view expo time --jsonHere are the last ten 57.0.x releases, as of 29 August 2026.
| Version | Published (UTC) |
|---|---|
| 57.0.9 | 2026-07-29 19:30 |
| 57.0.10 | 2026-08-04 07:40 |
| 57.0.11 | 2026-08-06 11:24 |
| 57.0.12 | 2026-08-10 15:43 |
| 57.0.13 | 2026-08-14 14:25 |
| 57.0.14 | 2026-08-17 10:48 |
| 57.0.15 | 2026-08-20 10:50 |
| 57.0.16 | 2026-08-24 07:56 |
| 57.0.17 | 2026-08-26 20:02 |
| 57.0.18 | 2026-08-28 10:48 |
Nine releases between 29 July and 28 August. Thirty days, so roughly one patch every three days.
The practical consequence is that a patch number you noted down a few days ago is quite likely no longer the newest one. If you carry a number over from a blog post, a changelog you skimmed last week, or your own notes, your starting assumption may already be stale. Ask the registry each time instead.
The range in package.json does not decide what you get
Next, what does each range style actually resolve to? I made an empty project with nothing but expo in dependencies, and asked npm to generate only the lockfile.
npm install --package-lock-onlyFive different specifiers, same day, same registry.
| Specifier in package.json | Resolved version |
|---|---|
57.0.9 | 57.0.9 |
~57.0.9 | 57.0.18 |
^57.0.9 | 57.0.18 |
57.0.x | 57.0.18 |
"*" (unconstrained) | 57.0.18 |
Tilde and caret both landed on 57.0.18, and so did the wildcard forms. While no new minor has been published, the difference between ~ and ^ simply does not surface.
The two are not equivalent, though. A tilde allows patch updates within the minor you named, so ~57.0.9 will accept 57.0.18 but stop before 57.1.0. A caret allows minor updates too, so ^57.0.9 would happily take 57.1.0 or 57.4.2 once those exist. Right now no 57.1.x has been published, which is the only reason both rows read the same. The day the first new minor lands, a project on a caret starts pulling in changes the tilde project will not see, without anyone editing a file. That is worth knowing before you decide which one you want in a project you ship from.
But notice what this table is really measuring: what npm picks when it resolves from scratch. It says nothing about what happens when you run npm install in a project that already has a lockfile — which is every real project.
The lockfile is what decides
So I ran the same check starting from an existing lockfile.
- Pin
"expo": "57.0.9"and generate the lockfile. - Edit package.json only, changing the range to
"expo": "~57.0.9". - Run
npm installwith the lockfile still in place.
The output:
step 1, pinned lock: 57.0.9
step 2, install with lock: 57.0.9
step 3, install without: 57.0.18Step 2 is the whole answer. Even with ~57.0.9 written in package.json, npm keeps 57.0.9 as long as the locked version still satisfies the range. It has no reason to move, so it doesn't.
Step 3 is the same package.json with the lockfile deleted first. Now 57.0.18 goes in. Which means that if package-lock.json is not committed to Git, your machine and your build server can quietly install different versions of Expo from identical source. It is the same shape of problem as filenames that look identical on your Mac and differ on the build server, which I wrote about in Sunset.png and sunset.png: one file on your Mac, two on the build server.
If you exported your code from Rork and started developing locally, check that package-lock.json made it into your first commit. If it lands in your ignore file, your dependency versions drift on every build. Write .gitignore Before You Run git init on an Exported Rork Project covers the setup.
npm ci will tell you when they disagree
A mismatch between package.json and the lockfile gets silently reconciled by npm install. When you want to hear about it, use npm ci.
Here is the real output with the lockfile on 57.0.9 and package.json asking for ~57.0.15:
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync.
npm error Invalid: lock file's expo@57.0.9 does not satisfy expo@57.0.18It stops. That refusal is exactly why build scripts and CI pipelines should use npm ci rather than npm install — failing at the point of disagreement costs far less than tracing a mystery difference days later.
When you just want to read the version currently locked, go straight to the file:
node -p "require('./package-lock.json').packages['node_modules/expo'].version"Cloud build services follow the same rule. EAS Build installs from the lockfile in your repository, so a lockfile that has not moved since March produces a March build today, no matter what range sits above it in package.json. Nothing warns you, because from npm's point of view nothing is wrong.
If your project uses a different package manager the file name changes but the logic does not. Yarn writes yarn.lock, pnpm writes pnpm-lock.yaml, and both resolve from the lock first and the range second. Whichever one you have, that file is the honest record of your dependency versions, and package.json is closer to a statement of intent.
What to run when you do decide to upgrade
Upgrading is an explicit act. Name the version, and the lockfile updates along with it because the new specifier falls outside the locked range.
npm install expo@57.0.18
npx expo install --fixThe second line realigns the packages that Expo manages so they match the combination the SDK expects. Bumping expo alone and leaving everything else where it was gets you a set of versions nobody tested together.
One more thing if you keep native directories in your repository: running prebuild after an upgrade can wipe hand-edited native changes. Take an inventory before you upgrade rather than after — Find the native edits expo prebuild will erase before you upgrade to SDK 57 walks through it.
The ordering lesson generalises. Before you go hunting through your own code for a memory problem, confirm which versions you are actually running, which is the same argument I made in Count your worklets before you start deleting your own code.
Start here today
Close package.json and open package-lock.json instead. Read one line: the version under node_modules/expo. That single line tells you whether your app is at the front of the queue or nine patches behind it.
I never checked, and left a fixed regression sitting in one of my indie developer builds for a month. If this saves you that detour, it was worth writing down.