TL;DR: Upgrading the Unity version of a project that real people are using is not a menu click, it is a small migration project. The editor upgrade itself is usually the easy part. What breaks is everything around it: packages that moved, render pipeline and input changes, native plugins, platform SDK requirements, and behaviour that changed without an error. My approach, after more than five years of Unity work across AR platforms, cross-platform SDKs and a live sports-tech product, is simple: upgrade on a branch, one major step at a time, with a known-good build to compare against, and ship the result through the same staged release you would use for a risky feature. Never upgrade and add features in the same release.
Every Unity project eventually has the conversation. The current editor version is getting old, a store or platform requirement is coming, a package you need only supports newer versions, or a bug you keep hitting is fixed upstream. Someone says "let's just upgrade", and a week later the team is staring at a build that compiles fine and behaves subtly differently on one device.
I have done this on projects I started and, more often, on projects I inherited, including as CTO at RAQTS, where the Unity layer was part of a live product with players and facilities depending on it. This is the process I use.
Why Unity upgrades are riskier than they look
On a web project, a framework upgrade mostly fails loudly. Types break, tests fail, the build stops. Unity fails quietly far more often, for a few structural reasons.
- The editor rewrites your project. Opening a project in a newer version reimports assets and can reserialise scenes, prefabs and project settings. The diff is huge, and most of it is noise that hides the few changes that matter.
- Packages move independently. The render pipeline, input system, XR and AR packages each have their own version rules. A new editor often forces new package versions, and those carry their own breaking changes.
- Native code sits underneath. Plugins compiled for one toolchain, Android Gradle and SDK settings, and iOS Xcode project generation all shift between versions.
- Behaviour can change without an error. Physics, lighting, shader compilation, UI layout and timing can all differ slightly. Nothing throws. The app just feels different.
That last point is the real danger. A compile error is cheap. A change in feel that reaches users is expensive, because nobody notices it in the editor.
Step 1: decide why you are upgrading, and to what
"Newer is better" is not a reason. A good reason is specific: a store requirement, a platform SDK you need, a package that will not install otherwise, or a fixed bug that is costing you. The reason sets the target.
My default target is the latest long-term support release that satisfies the reason, not the newest version available. LTS releases get fixes for longer and change less underneath you. For an interactive product that has to work every night, boring is a feature.
Write the reason down. When the upgrade gets painful halfway through, and it usually does, a written reason is what stops the team from either abandoning it or quietly expanding it into a rewrite.
Step 2: get a known-good baseline before touching anything
Before upgrading, I want to be able to answer one question at any point: is this different from what we had?
That means:
- A tagged build of the current version, produced by CI, installed on the real test devices and kept there. If the project does not have reproducible builds yet, fix that first. I wrote about the order to do it in my Unity CI/CD pipeline post.
- A short list of flows that matter, the ten or so things a user actually does, written down so anyone can walk them.
- A few numbers worth comparing: build size, load time to the first interactive screen, and frame time on your weakest supported device.
Without a baseline, every report after the upgrade turns into an argument about whether it "was always like that". With one, you install both builds side by side and look.
Step 3: audit what will break before it breaks
An hour of reading saves days of debugging. Before opening the project in the new editor, I list:
- Every package in the manifest, and whether a compatible version exists for the target editor.
- Every native plugin and third-party SDK, especially analytics, ads, payments, networking and anything with Android or iOS native code. These are the most common blockers, because you are waiting on someone else's release.
- Render pipeline and custom shaders. Custom shaders written against an older pipeline version are a frequent source of silent visual changes.
- Input handling. Projects mixing the old input manager with the newer input system tend to surface problems here.
- Platform settings: Android minimum and target API levels, Gradle setup, iOS minimum version, and any build post-processing scripts that edit the generated Xcode or Gradle projects.
Anything with no compatible version is a decision before the upgrade starts, not a surprise halfway through. Sometimes the honest answer is that an abandoned plugin has to be replaced first, as its own piece of work. This is the same audit habit I use when inheriting a codebase in the first week: find the thing nobody owns before it finds you.
Step 4: upgrade on a branch, one major step at a time
The mechanics I follow:
- Create a dedicated branch. Nothing else lands on it except the upgrade.
- Close the editor, clear the Library folder, and open the project in the new version so the reimport is clean.
- Commit the automatic changes on their own, before fixing anything. That commit is the editor's work. Everything after it is yours, and reviewers can tell the two apart.
- Fix compile errors, then warnings that mention obsolete APIs. Deprecation warnings are next year's compile errors.
- Jump one major version at a time if you are several behind. Skipping straight across many versions stacks every migration into one unreadable diff.
Keep the branch short-lived. A long-running upgrade branch fights every feature merged in the meantime, and resolving merge conflicts in scene and prefab files is one of the least pleasant jobs in Unity.
Step 5: test on devices, not in the editor
The editor is the place where upgrades look fine. Real devices are where they are not.
I test the upgraded build against the baseline on the same physical devices, side by side, walking the written flows. Things I specifically look for:
- Visual differences: lighting, colour, shadows, transparency, UI scaling and safe areas.
- Performance on the weakest device, especially frame time and memory. For standalone headsets this matters even more, as I covered in Unity performance on standalone VR headsets.
- First launch after an update, not just a clean install. Saved data, cached content and player preferences must survive the upgrade.
- Platform-specific features: AR tracking, permissions prompts, push notifications, purchases, deep links, backgrounding and resuming.
For AR work, this means testing in real conditions rather than at a desk. The lessons in field testing AR apps apply directly, because tracking changes from a package update rarely show up indoors under good light.
Step 6: ship it like a risky feature
An engine upgrade is one of the riskiest releases a Unity product makes, and it deserves the same care as one.
- Release it alone. No new features in the same build. If something regresses, you want exactly one suspect.
- Use staged rollouts on Google Play and phased release on the App Store, so a problem reaches a small share of users first.
- Watch crash and error reporting closely for the first days, comparing against the previous version's baseline.
- Keep the backend tolerant of the old version, because many users will stay on it for weeks. That principle is covered in depth in supporting old mobile app versions.
On mobile you cannot roll back a shipped build the way you roll back a web deploy. The store review cycle is the real constraint, so the testing bar before submission has to be higher than it feels necessary.
How often to upgrade
The cheapest upgrade is a small one. A project that moves to each new LTS within a few months of release does a series of manageable migrations. A project left for several years eventually faces one huge, risky jump, usually forced by a store deadline at the worst possible time.
My rule of thumb for live products: plan an upgrade at least once a year, schedule it during a quiet period, and treat it as maintenance in the roadmap rather than something squeezed between features. Platform requirements, such as Google Play raising the target API level it accepts, arrive on a calendar. It is better to be ahead of that calendar than chasing it.
The short version
- Have a specific reason, and target the LTS release that satisfies it.
- Build and keep a known-good baseline on real devices before touching anything.
- Audit packages, plugins, shaders, input and platform settings first.
- Upgrade on a dedicated branch, one major version at a time, with the editor's automatic changes in their own commit.
- Test on devices against the baseline, including updating over an existing install.
- Ship the upgrade alone, staged, with crash reporting watched.
None of this is glamorous, and that is the point. Upgrades that go well look like nothing happened. That outcome comes from treating a Unity upgrade as an engineering project with its own plan, rather than something a team does on a Friday afternoon.
I build Unity, AR and XR products and lead engineering as a fractional CTO, from AR platforms at ARCortex and cross-platform SDKs at Geonode to a live sports-tech product at RAQTS, alongside AI agents for founders across the US, UK and Europe. If your Unity project needs an upgrade or a steady pair of hands, more about my work here, or book a call.