When a build fails, the first place you look is whatever you touched. I did that for years.
But once you are running a few apps side by side, a day arrives when nothing on your side changed and the result changed anyway. Same commit. Same dependencies. What moved was the Mac on the other end of the pipe.
Scroll an EAS Build log all the way back to the top and there is a quiet section called Spin up build environment. It names the image that build actually ran on. For a long stretch I had never read that line once.
The default when you leave it out is not latest
This is the part I see misread most often.
If you do not put an image field in eas.json, EAS Build runs the job on the auto alias. Not latest. The auto alias picks an image based on your project configuration, your Expo SDK version, and your React Native version.
So the default behaviour is not "always newest." It is "chosen to suit your SDK." That sounds calm, and mostly it is. The flip side is that the image changes the moment you bump your SDK.
The value you can set is either a full image name or one of the aliases: auto, latest, or a per-SDK alias such as sdk-57. They behave differently from one another.
| What you set | When its contents move | When it fits |
|---|---|---|
Full name (e.g. macos-tahoe-26.5-xcode-26.6) | Almost never; minor updates only | You want a frozen environment |
auto (default) | When you raise the SDK or RN version | You want to follow Expo's judgement |
Per-SDK, e.g. sdk-57 | With every new SDK release | You track environments by SDK |
latest | With every new image release | You want the newest Xcode early |
latest does not mean "stable." It means "whatever image is newest gets this label." Read it as the stable option and your reasoning ends up pointing the wrong way.
What actually swaps when one SDK alias moves
That stays abstract until you put two images next to each other, so here are the iOS images for SDK 56 and SDK 57 from Expo's published build server list.
| Item | sdk-56 (macos-tahoe-26.4-xcode-26.4) | sdk-57 (macos-tahoe-26.5-xcode-26.6, latest) |
|---|---|---|
| Xcode | 26.4 (17E202) | 26.6 (17F113) |
| macOS | Tahoe 26.4.1 | Tahoe 26.5.2 |
| Node.js | 22.22.2 | 22.23.1 |
| pnpm | 10.33.3 | 11.9.0 |
| fastlane | 2.233.1 | 2.236.1 |
| Maestro | 2.5.1 | 2.6.1 |
The line that stops me is pnpm: 10.33.3 to 11.9.0, straight across a major version. One alias step and your package manager changes generation. Android does the same thing. Going from ubuntu-26.04-jdk-17-ndk-r27b (sdk-56) to ubuntu-26.04-jdk-17-ndk-r27b-sdk-57 takes pnpm from 10.33.3 to 11.9.0 and node-gyp from 12.3.0 to 13.0.0.
A node-gyp major moving under a project with native modules is quiet, but it is not small. All of that rides underneath a one-line changelog entry that says "bumped Expo SDK."
These figures are what Expo's build server infrastructure page listed at the time of writing. Images get added regularly, so open that page yourself before you make a call on it.
It is worth being precise about which of these you can actually control elsewhere. Node.js you can usually influence through your project setup. Xcode, the macOS version, and the Ruby that fastlane runs on are decided by the image and nothing else. That is the practical reason the image field exists: it covers the pieces there is no other knob for.
Pull the image name out of your last green build
Before deciding whether to pin, you need to know where you already are. The order I use:
- Open your most recent successful build in the EAS dashboard
- Scroll the log to the
Spin up build environmentsection - Copy the image name printed there
If you are running on auto, that name is the environment your app is genuinely shipping from today. It is not written in eas.json, so the log is the only place it exists.
Once you have the name, it drops straight into your config.
{
"build": {
"production": {
"ios": {
"image": "macos-tahoe-26.5-xcode-26.6"
},
"android": {
"image": "ubuntu-26.04-jdk-17-ndk-r27b-sdk-57"
}
}
}
}You can also split it per profile. I prefer pinning production while letting a preview profile roll forward, so something in my setup meets the new environment early. Keeping one place that is allowed to break is what lets a pinned production build avoid quietly falling behind.
{
"build": {
"preview": {
"distribution": "internal",
"ios": { "image": "latest" }
},
"production": {
"ios": { "image": "macos-tahoe-26.5-xcode-26.6" }
}
}
}When pinning helps, and when it stops helping
Pinning is not a free win. Both directions carry a cost.
What you buy is reproducibility. Rebuild the same commit next month and it goes through the same Xcode and the same fastlane. That removes a specific kind of bad afternoon: a build failing right before submission with no obvious cause.
What you owe is attention. Expo aims to support the stable Xcode releases that App Store Connect will still accept, which in practice means the newest one and the one before it, until Apple raises its minimum Xcode requirement again. When that requirement moves, a pinned image eventually stops being submittable. Pinning is not "you never have to decide." It is "you choose when to decide."
So if you pin, put the date and the reason in the commit message. I have skipped that step more than once and then spent an evening working out why a particular image name was sitting there.
There is a middle position that suits a lot of solo projects. Pin nothing, but read the image name after every successful production build and keep the last few in a note. You give up reproducibility, and in exchange you always know what changed and when, which is most of what pinning was protecting you from. If your release cadence is slow enough that months pass between builds, I would pin. If you ship every couple of weeks, the note may be enough.
What I would avoid is the third state: no pin, no note, and no habit of reading the log. That is where a failing build turns into an afternoon of bisecting your own code for a change that was never yours.
Counting what your local toolchain actually has, rather than what you assume it has, comes up from another angle in checking which expo version your app is really on.
One thing worth deciding before Xcode 27
iOS 27 is reported to ship on September 14. As new Xcode releases follow, both latest and the per-SDK aliases will point somewhere else than they do today.
One task is enough for now. Open the log of your last successful build and copy the image name from Spin up build environment. Just copy it. Whether to pin is a decision you can make afterwards, with the name in front of you.
If you are about to start device checks, pairing this with the steps in touching your published app on a spare iPhone before the release makes the sequencing easier. And if the Android deadline is running alongside, confirming your effective targetSdkVersion belongs in the same week.
Thank you for reading. Build environments are the part nobody looks at while things are working. Opening it once during a calm week tends to pay for itself during a loud one.