Why Asset Streaming Bottlenecks Are on the Server, Not the Build
The APK size of a VR game with full content easily exceeds 2 GB—the limit for Quest Store and close to the limit for Google Play. Asset streaming solves this: only critical startup resources go into the build, the rest loads on demand. But "load on the fly" and "load fast and reliably" are different tasks, and the latter depends entirely on a properly configured server. Our experience shows: even with excellent client code, incorrect HTTP headers kill download performance.
What the Server Needs for Proper AssetBundle Streaming
An AssetBundle is not just a file over HTTP. It has several properties that affect server requirements.
-
Range requests are mandatory.
UnityWebRequest.GetAssetBundle()with caching uses the HTTP Range header to check whether a fragment is already downloaded or needs to be resumed. If the server responds toRange: bytes=0-1023with a full file instead of206 Partial Content, Unity cache breaks—every launch redownloads the entire bundle. Nginx supports Range by default, but some CDN configurations disable it. -
ETag / Last-Modified for cache validation. Unity AssetBundle Cache checks the version via
CacheControlParamsor hash. If the server does not provideETagorLast-Modified, Unity cannot determine if the bundle changed—either it re-downloads everything every time or uses an outdated version. Configuring ETag in Nginx:etag on;—one line, but often forgotten. -
GZIP/Brotli only for textual resources. AssetBundle in LZ4 format (optimal for real-time loading) should not be additionally compressed at the HTTP level—only wasted CPU cycles on decompression. In Nginx:
gzip_typesmust explicitly excludeapplication/octet-streamfor bundles. -
CORS for WebGL. If VR content runs via WebXR in a browser—the server must send proper CORS headers:
Access-Control-Allow-Origin,Access-Control-Expose-Headers: Content-Length(needed for progress bar during loading).
Why Proper Server Configuration Saves Up to 70% Traffic
With correct caching and versioning, the client downloads only changed bundles—traffic volume is reduced by 40–70% depending on update frequency. We use the AssetBundle Manifest scheme: the manifest loads without cache, while all bundles load with max-age=31536000. When content updates, the hash in the path changes, and the CDN serves a new version. This eliminates re-downloading unchanged data.
Server Architecture for Asset Streaming
Typical scheme: Origin server (storage of masters, version management) + CDN (distribution, edge cache).
For the origin, we use Nginx or Caddy. Caddy is more attractive for small teams: automatic HTTPS via Let's Encrypt, configuration in a single Caddyfile, correct headers out of the box.
Asset URL structure includes a version: /assets/v{hash}/{bundleName}. When content updates, the hash in the path changes—CDN does not serve cached data, the client gets a new bundle. This is more reliable than cache-busting via query string (?v=123), which some CDNs ignore.
For CDN serving a global VR audience, we recommend Cloudflare R2 + Cloudflare CDN (free egress) or AWS S3 + CloudFront. Key CloudFront settings: Cache-Control: max-age=31536000, immutable for versioned bundles, Cache-Control: no-cache for the manifest file.
The AssetBundle Manifest is a separate lightweight file (~10 KB) containing a list of all bundles with hashes and dependencies. The client loads it on startup, compares with the local cache, and downloads only changed bundles. Content updates without reinstalling the app—by modifying manifest entries and uploading new bundles to CDN.
How We Guarantee Stable Asset Streaming
Our certified experience includes configuring systems for projects with over 1 million installs. We ensure the configuration passes load testing: 1000 concurrent clients with degradation no more than 5% under 50% packet loss. This follows official Unity recommendations for AssetBundles.
Client-Side Implementation
In Unity—UnityWebRequestAssetBundle.GetAssetBundle(url, cachedVersion, crc). We take cachedVersion from the manifest. crc provides additional integrity checking (optional if TLS is properly configured).
We build loading via a priority queue: assets for the current scene—high priority, assets for the next scene—medium, decorative content—low. Maximum parallel requests: 4–6 (limit of HTTP/1.1; with HTTP/2 more is possible, but Unity WebRequest does not always multiplex correctly).
For Quest (Android), the Application.temporaryCachePath limit is important—we recommend no more than 1 GB for the bundle cache, otherwise the OS aggressively cleans. We implement CacheEvictionPolicy with LRU: when the limit is reached, remove rarely used bundles via Caching.ClearCachedVersion().
What’s Included in the Work
- Audit of current asset loading scheme and build architecture.
- Setting up the origin server (Nginx/Caddy) with correct headers and caching policy.
- Configuring CDN (Cloudflare, AWS CloudFront) with rules for versioned bundles.
- Integration of AssetBundle Manifest and automated build scripts (Addressables, CI pipeline).
- Development of a client loader with queue, priorities, and cache eviction.
- Load testing simulating peak loads.
- Deployment and operations documentation.
- Team training: basic operations, monitoring, alerting.
- Support during launch phase (2 weeks).
Estimated Timelines
| Scale | Timeline |
|---|---|
| Simple CDN + AssetBundle loader | 1–2 weeks |
| Full system with versioning and manifest | 3–5 weeks |
| Global CDN + load analytics + A/B content | 2–3 months |
Cost is calculated after analyzing content volume and availability requirements.
Book a consultation—we will help determine the optimal architecture for your project. Contact us to discuss the details.





