Unity Addressables: What I Wish I'd Known Before Leaving Resources Behind
The project that finally forced me onto Addressables had a Resources folder weighing 900 MB. Nobody had planned that; Resources folders are never planned. They accrete, one “I just need to load this by name” at a time, over three years of live operation. By the end, our Android app started in eleven seconds, couldn’t ship a new event banner without a store review, and had a memory profile that made low-end devices sweat before the first tap.
I’d been putting the migration off for a year because Addressables has a reputation: powerful, poorly understood, easy to hold wrong. Having now taken two live games through it, I think the reputation is half-earned. The API is genuinely small. What hurts people is that Addressables makes you think about asset lifetime, which Resources let you ignore, and it lets you find that out in production if you skip the fundamentals. So here are the fundamentals, in the order I wish I’d learned them.
Why the Resources folder doesn’t scale
Resources is seductive because it’s one line: Resources.Load<Sprite>("banners/summer"). Here’s the bill for that line, itemised.
Everything under every Resources folder ships in every build, referenced or not, because a string might load it someday. Ours carried art for events that had ended two years earlier; I’ve told that story in my post on shrinking Unity build sizes. At startup, Unity builds an index of the whole folder before your first scene breathes, which was most of our eleven seconds. Loading is name-based, so renaming a file silently breaks call sites at runtime. And unloading is all-or-nothing: individual Resources.UnloadAsset calls for things you can enumerate, or a full Resources.UnloadUnusedAssets() sweep with its frame hitch. There is no “release this screen’s assets” granularity, because Resources has no concept of a group of things that live and die together.
That last one is the real disease. The others are symptoms.
Addresses, groups, bundles, catalogs: the four layers
Addressables is four ideas stacked, and every confusing behaviour becomes predictable once you keep them separate.
An address is a string key you assign to an asset. Unlike a Resources path, it’s attached to the asset’s GUID, so renaming or moving the file changes nothing. A group is an authoring-time bucket of addressable assets; groups are yours to design, and they are where all the strategy lives. At build time each group becomes one or more AssetBundles, the actual files that ship or sit on a server. And the catalog is the lookup table mapping every address to its location in some bundle, loaded when Addressables initialises.
The catalog is the layer that changes what’s possible: it can point at bundles on a CDN as easily as bundles on disk, which is how you ship content without an app update. But before any of that, the layer that decides whether your game runs well is the group, because groups control memory.
How Addressables memory actually behaves
Here is the sentence I’d tattoo on every Addressables tutorial: memory is managed per bundle, and a bundle stays loaded while anything inside it is still in use. Addressables reference-counts every load you make. Load a sprite, its count goes up; release it, count goes down; when every asset in a bundle hits zero, the whole bundle can unload. Until then, the whole bundle stays.
Put a hundred boss portraits in one group with default packing, load one portrait, and you’re paying memory for a bundle containing all hundred. That single mechanic explains most “Addressables is using way more memory than I expected” threads. It’s also entirely steerable: each group has a Bundle Mode, packed together, together-by-label, or separately, and choosing it is the job. My rule of thumb after two migrations: things that are always used together (a level’s environment set) pack together; things used one-at-a-time out of a large pool (skins, portraits, banners) pack separately; and I want a real reason before a group mixes the two.
The other memory landmine is duplication. If two groups both reference some texture that isn’t itself addressable, that texture is silently copied into both bundles, costing size on disk and memory when both are loaded. The Analyze window has a “Check Duplicate Bundle Dependencies” rule that finds these; run it every time you restructure groups, because the fix (make the shared dependency addressable in its own group) takes a minute and the symptom without it is invisible.
Loading assets, and the discipline of releasing them
The API side is small enough to show almost completely:
AsyncOperationHandle<GameObject> handle =
Addressables.LoadAssetAsync<GameObject>("Enemy_Boss_Fireborn");
GameObject prefab = await handle.Task;
// ... later, when this screen/level is done with it:
Addressables.Release(handle);
Everything is async, because the bundle might be on a CDN; there is no blocking load to fall back on, and that’s the single biggest workflow adjustment coming from Resources. The handle you get back is not ceremony. It is your claim on the asset, and Release is you giving that claim back. Hold a handle and never release it, and its bundle is immortal; release it while something still uses the asset, and you get the fun crash where a sprite goes pink two screens later.
The discipline that works is pairing every load with an owner whose lifetime matches: a level object that loads its assets on entry and releases its handles on exit, a screen that does the same. Store handles, not results. If you find yourself unable to say who owns a load, that load will eventually be a leak, and it’ll turn up exactly the way the leaks did in my memory profiling walkthrough: stair-stepping native memory, this time with bundle names attached. For instantiated prefabs there’s InstantiateAsync paired with ReleaseInstance, which counts per-instance and keeps you honest.
Scenes, labels, and the synchronous escape hatch
Three API corners round out the working set. Scenes can be addressable too, loaded with Addressables.LoadSceneAsync, and they’re secretly the easiest win in the whole system: a scene is a natural ownership boundary, so unloading it releases its handle and everything the scene pulled in follows. If your game is structured as scene flows, you inherit correct release discipline almost for free, and big scenes move out of the base build into content you stream.
Labels are the batch tool. Tag twenty boss portraits with a label and LoadAssetsAsync fetches the set in one call with one handle, which beats twenty string addresses stitched together with your own bookkeeping. Labels are also how “pack together by label” grouping gets its meaning, so the loading code and the memory strategy end up speaking the same vocabulary.
And when something truly must be synchronous, WaitForCompletion() exists. It blocks the main thread until the load finishes, which at a loading screen is fine and mid-gameplay is a hitch you handed yourself. I allow it in two places: boot sequences, and editor tooling. Anywhere else, the async structure isn’t Addressables being difficult, it’s the honest shape of loading content that might live on a server on the other side of the planet.
Why it works in the editor and fails on the device
Addressables in the editor has a dropdown most people discover only after their first broken build: Play Mode Script. The default, “Use Asset Database”, serves every load straight from the editor’s asset database. No bundles, no catalog lookups that matter, effectively no failures. Which means your entire Addressables setup can be misconfigured and play mode will cheerfully hide it: missing entries, bad remote paths, groups you forgot to include, none of it surfaces.
Then the device build initialises from a real catalog, the address isn’t in any built bundle, and LoadAssetAsync hands you a failed handle at runtime. This is a perfect specimen of the genus I described in works in the editor, breaks in the build. The vaccine is the third play mode option, “Use Existing Build”: run your content build, then play against the actual bundles and the actual catalog in-editor. It’s slower to iterate, so I don’t live in it, but no release candidate leaves without a session there. And since the content build now decides whether the game works, it belongs in automation, not muscle memory.
One more trap in the same family: AssetReference fields, the drag-and-drop way to hook addressables into the Inspector, store GUIDs and fail with the same silence as every serialized reference when the target asset is deleted. Nothing complains at edit time; the load fails on device. I dissected that whole category in Unity’s silent failures, and Addressables entries are on the list of things worth auditing before a release.
Remote content, briefly and honestly
The headline feature, updating content without an app update, works and is worth having: point a group’s load path at your CDN, ship catalogs with your bundles, and a live-ops banner becomes an upload instead of a review cycle. Our event turnaround went from a week to an afternoon.
Do a full dress rehearsal before trusting it: host a content build on any local static server, point the remote load path at it, and run the game from a real build against that “CDN.” Every failure mode you’d otherwise discover in production, stale catalogs, wrong paths, the cache serving old bundles, shows up in an afternoon on localhost, where it’s funny instead of expensive.
The honest part: remote content converts Addressables from a build feature into a small operations habit. Catalog versions, cache behaviour on players’ devices, the update-restrictions check before you rebuild static groups, deciding what’s allowed to change remotely at all. None of it is hard, all of it is real work, and I’d tell any team to ship their first Addressables release fully local. You bank the memory control and startup wins immediately and take on the CDN when there’s a live-ops reason.
The migration order that worked
Both times, the sequence that avoided disaster was the same. First, wrap every Resources.Load call site behind one small loading service, changing nothing else; this is dull and takes a day and makes everything after it safe. Second, migrate one self-contained content family end to end, banners or audio, and get its group structure and release discipline actually right on a device build. Third, expand family by family, running the duplicate-dependency check each time. The Resources folder gets deleted last, ceremonially, when the service’s Resources code path has been dead for a full release.
Our eleven-second startup ended at four. The 900 MB became bundles we load when they’re relevant, and low-end devices stopped sweating. Addressables didn’t do that; deciding who owns every loaded asset did. The package just refuses to work until you’ve decided, which, a year in, I’ve come around to seeing as the feature.
Common questions
Why is Unity Addressables using more memory than expected?
Because memory is managed per bundle, not per asset. A bundle stays loaded while anything inside it is still in use, so loading one portrait from a group of a hundred packed together keeps the whole bundle resident. Switch that group's Bundle Mode to Pack Separately for assets used one at a time out of a large pool.
Why do Addressables work in the editor but fail in a build?
The editor's default Play Mode Script, Use Asset Database, serves every load straight from the asset database, so missing entries and bad remote paths never surface. Switch to Use Existing Build after running a content build, and play mode uses the same bundles and catalog a device would.
Can I load Addressables synchronously in Unity?
Yes, WaitForCompletion() blocks the main thread until the load finishes. That is fine in a boot sequence or editor tooling, but mid-gameplay it is a frame hitch you handed yourself. Everything else in the API is async because the content might live on a CDN.
How do I update Unity game content without an app store review?
Point an Addressables group's load path at a CDN and ship catalogs alongside your bundles; the catalog tells the game where to fetch content at runtime. Rehearse the whole flow against a local static server first, because stale catalogs and caching surprises are far cheaper to find on localhost than in production.
How do I migrate from Resources to Addressables safely?
Wrap every Resources.Load call site behind one loading service first, changing nothing else. Then migrate one content family end to end, get its group structure and release discipline right on a device build, and expand family by family. Delete the Resources folder only after its code path has been dead for a full release.