# React Native OTA at Scale: Caching, Rollout Cohorts, and Update Telemetry

> How caching, deterministic rollout cohorts, immutable artifacts, and off-path telemetry keep React Native OTA delivery predictable as fleets grow.

Canonical: https://bundledrop.app/resources/react-native-ota-at-scale

Published: 2026-10-14

Updated: 2026-10-14

Category: Engineering

Topics: OTA Operations, Delivery, Rollouts & Recovery

React Native OTA systems have an unusual traffic shape. A team may publish a few releases each week while thousands or millions of installed applications check for updates, download artifacts, report outcomes, and start from different release histories. The write path is occasional. The read path can sit directly beside application launch.

Scaling that system is not only a question of adding servers. The release model has to produce stable, cacheable state; rollout membership has to remain deterministic; artifact delivery should avoid unnecessary control-plane work; and telemetry must not become another dependency that blocks startup.

The useful objective is predictable behavior as the fleet grows:

> The same compatible installation state should resolve the same release decision, obtain immutable bytes efficiently, and report enough evidence for operators to understand adoption and failure—without putting analytics or expensive release-history work on the launch path.

This guide is provider-neutral. It describes architecture boundaries and verification questions, not a claim that every OTA platform implements them in the same way.

## OTA scale is mostly read-path scale

The number of update checks is driven by installed applications, launch frequency, resume policy, connectivity, and retry behavior. It is not proportional to the number of releases. One publication can be read repeatedly by an enormous fleet.

That asymmetry changes which work belongs where:

| Work | Appropriate path | Why |
| --- | --- | --- |
| Build and validate artifacts | Publish path | Expensive work happens once per release rather than once per device |
| Materialize eligible release state | Publish or control path | Devices should not reconstruct release history on every check |
| Select platform, runtime, and channel lane | Device read path | These inputs define the small compatibility surface being queried |
| Assign rollout cohort | Deterministic read-path calculation | The same installation should not move unpredictably as request volume grows |
| Deliver bundle or patch bytes | CDN or object-delivery path | Immutable large objects are well suited to distributed caches |
| Aggregate analytics | Asynchronous telemetry path | Reporting should not delay resolution, activation, or startup |

Optimizing only artifact bandwidth misses much of the problem. A tiny patch does not help if every launch waits on a slow control-plane query, a global database scan, or a synchronous analytics write.

## Separate release management from the device read path

The control path answers operator questions: which artifact was uploaded, which channel owns it, whether it is published, what percentage is exposed, which audience rules apply, and whether the release has been paused or rolled back.

The device path needs a much smaller answer: for this project, platform, native runtime, channel, installation state, and current artifact, is there an eligible target?

That does not require exposing private architecture or creating two independent sources of truth. It requires publishing a read model whose identity and invalidation rules follow the release model. A device check should not need to enumerate unrelated projects, platforms, runtimes, or historical releases before it can learn that no update is available.

A useful lane key commonly includes stable dimensions such as:

- project or application identity;
- platform;
- native runtime or compatibility value;
- release channel;
- any application-defined context that legitimately changes eligibility.

Audience properties and rollout inputs require more care because they can fragment cacheability. Treat every new selection dimension as an operational cost. If a property does not change which release can be returned, it should not be part of the cache key.

## Cache shared release state, not personalized secrets

HTTP caching is most effective when many devices request the same representation under one stable key. [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) defines the freshness and validation model used by shared caches, including `Cache-Control`, validators, and revalidation.

OTA release state benefits from the same discipline:

- use canonical URLs or keys for one release lane;
- make versioned artifacts immutable rather than overwriting bytes in place;
- attach validators or generations to mutable lane state;
- define whether stale state is acceptable and for how long;
- invalidate or supersede lane state when publishing, pausing, revoking, or rolling back;
- avoid placing credentials or per-install identifiers in shared cached content.

Caching the wrong abstraction is dangerous. A response containing personalized eligibility cannot be shared merely because the bundle itself is common. Conversely, forcing every device through a personalized API call when the underlying lane state is identical gives up useful cache leverage.

One practical design is to cache authenticated shared release state, then evaluate narrowly scoped installation-specific rules locally or through a bounded resolver. Another is to generate a small number of audience variants. The correct choice depends on the product's targeting semantics, but cache behavior must be part of that design rather than added afterward.

## Treat invalidation as release correctness

“Cache invalidation is hard” is not a sufficient operating model for executable updates. Publication, pause, rollback, and revocation change what a device is allowed to select. The delivery system must define when those transitions become visible.

Useful questions include:

1. Does publishing create a new immutable generation or overwrite existing state?
2. How does a client distinguish newer state from stale replay?
3. What happens when an edge still holds a previous rollout percentage?
4. Can a rollback be hidden by a long freshness window?
5. What state can be used offline, and what requires a fresh authority decision?
6. Does an artifact remain addressable after it stops being eligible?

Artifact retention and eligibility are different. Keeping immutable bytes available can support rollback and devices with older histories without leaving that release selected for new checks. Deleting an object is not a substitute for changing authenticated release state, and changing release state should not depend on every edge deleting a large artifact immediately.

## Make rollout cohorts deterministic

A percentage rollout should expose a stable subset of eligible installations. Re-randomizing on every request causes devices to move in and out of the rollout, makes adoption difficult to interpret, and can repeatedly offer an update to an installation that intentionally remained outside the cohort.

A common model hashes stable inputs such as installation identity and release identity into a fixed bucket range. The release percentage then defines which buckets qualify. Expanding from 10% to 25% adds a predictable group without removing the original 10%.

The exact hash function is less important than the contract:

- the input identity is stable and privacy-appropriate;
- the same release and installation produce the same bucket;
- the result is uniformly distributed enough for the intended cohort size;
- percentage changes have understandable monotonic behavior;
- rollback or a new release identity does not accidentally reuse unhealthy state;
- operators can reproduce or explain an eligibility decision without storing a permanent assignment row for every installation.

Expo's [rollout documentation](https://docs.expo.dev/eas-update/rollouts/) illustrates why rollout state belongs to the release decision rather than download progress. A device outside the newest rollout may continue receiving the previous update. Bundle Drop's documented model similarly uses stable install buckets with newest-first fallthrough, described in [How staged rollouts, targeting, and rollback fit together](/resources/staged-rollouts-targeting-rollback).

## Keep immutable artifacts on the artifact path

JavaScript bundles, source maps, assets, and patches are larger than release-selection metadata and are read many times after being written once. They should have immutable identities and be deliverable independently of the operator-facing control surface.

An artifact path should support:

- stable content identity;
- long cache lifetimes for versioned bytes;
- range, retry, and interruption behavior appropriate to the storage layer;
- bounded authorization when artifacts are not public;
- integrity verification after download;
- full-artifact fallback when an optimized patch path is unavailable.

Do not treat a cache hit as proof that bytes are correct. CDN caching and artifact hashes solve different problems. The cache reduces repeated origin work; verification proves that the result matches the selected release.

Patch delivery can reduce egress and shorten the download window for devices with a suitable base. It also creates more possible source states. A large fleet will contain skipped releases, new installations, interrupted downloads, rollback histories, and offline devices. [Patch delivery with full-bundle reliability](/resources/patch-delivery-full-bundle-reliability) explains why the complete target must remain reachable even when most devices take a smaller route.

## Keep analytics off the launch-critical path

Telemetry is essential for operating OTA, but a metrics outage should not prevent an application from starting from a verified bundle. Update checks, download decisions, activation, and local recovery need defined behavior when analytics ingestion is slow or unavailable.

Useful patterns include:

- enqueue bounded events locally or in the service rather than synchronously aggregating them;
- make install and health receipts idempotent so retries do not inflate adoption;
- separate current release state from eventually consistent reporting views;
- bound retry frequency and retained event volume on the device;
- avoid attaching unbounded user properties or URLs to diagnostic events;
- degrade reporting independently from the release-selection decision.

This does not mean telemetry can be best-effort without visibility. Operators should monitor dropped, delayed, or duplicate reports as properties of the reporting pipeline. It means the failure domain should remain separate from application startup.

[OpenTelemetry's signal model](https://opentelemetry.io/docs/concepts/signals/) separates metrics, logs, and traces because they answer different operational questions. An OTA system benefits from the same discipline rather than forcing every outcome into one counter.

## Measure the release, not only the request

Request rate and CDN hit ratio describe infrastructure load. They do not tell a release engineer whether users are running the intended JavaScript.

A useful OTA telemetry model connects events to one immutable release identity and distinguishes stages:

| Signal | What it can establish |
| --- | --- |
| Update check result | Whether the device saw no update, an eligible target, or a selection failure |
| Transfer outcome | Whether patch or full bytes arrived and verified |
| Staged state | Whether the candidate is ready but not yet executing |
| Active identity | Which OTA or embedded bundle is currently running |
| Startup health | Whether a newly active candidate crossed the application's readiness boundary |
| Recovery event | Which candidate failed and which known-good state replaced it |
| Crash/error context | Whether failures correlate with one active artifact, platform, runtime, or cohort |

Do not equate “downloaded” with “adopted.” Activation policies, process restarts, connectivity, and user behavior create delay between availability, download, staging, and execution. [React Native OTA Source Maps with Sentry and Hermes](/resources/react-native-ota-sentry-source-maps) shows why crash reports also need the active OTA identity rather than only the app-store version.

## Bound retries and failure amplification

At fleet scale, a small client bug can become a traffic event. If every device retries immediately after a timeout, an origin recovery can trigger another spike before it stabilizes.

Exercise:

- exponential backoff with jitter for non-launch-critical retries;
- a maximum number of attempts per application session;
- server-directed or client-defined minimum check intervals;
- cancellation when the application moves to the background;
- request timeouts and response-size bounds;
- behavior when a CDN returns a persistent error;
- offline startup from the current known-good bundle;
- reauthorization without rebuilding or reselecting a different target silently.

Retries must preserve decision identity. Repeating an artifact request after authorization expires can be safe when the client reauthorizes the same authenticated selection. Silently resolving a different release halfway through one install makes the final state harder to verify.

## Test scale through failure semantics

Load testing an update endpoint is useful, but it does not establish production behavior by itself. The more revealing tests combine volume with state transitions:

1. Publish a release while many clients hold the previous lane generation.
2. Expand a rollout and verify that the original cohort remains included.
3. Pause or roll back while edges still cache prior state.
4. Corrupt a patch and confirm devices use the verified full target or remain on the current bundle.
5. Delay telemetry ingestion while checks and offline launches continue.
6. Expire artifact authorization during a download and verify bounded recovery.
7. Remove the control service temporarily while immutable artifacts remain reachable.
8. Simulate a retry wave and confirm backoff prevents synchronized amplification.
9. Compare selection, download, activation, and health counts rather than treating them as one funnel step.

Define acceptance criteria before the test: maximum stale-selection window, expected fallback, allowed launch delay, data-loss tolerance for telemetry, and the operator signal that declares the system unhealthy.

## Where Bundle Drop fits

Disclosure: I am the founder of Bundle Drop.

Bundle Drop's public model keeps release selection separate from transfer. Project, channel, platform, runtime version, publishing state, property targeting, and a stable rollout bucket determine the eligible release. A compatible device that does not qualify for the newest release can fall through to an older eligible release rather than receiving an incompatible or unauthorized target.

After selection, the device may receive a patch or a verified full bundle. Both routes converge on the same target hash before staging. Runtime observability exposes the active bundle identity so error reports can distinguish several OTA releases running under one native app version.

The maintained operational behavior is documented under [Managed Publishing](/docs/managed-publishing), [Staged Rollouts](/docs/staged-rollouts), [Observability](/docs/observability), and [Rollback](/docs/rollback). This article does not describe Bundle Drop's private service topology or claim traffic volumes that have not been published and measured.

## Scale the release model before the server count

React Native OTA scales well when the architecture respects its workload shape. Prepare expensive state once, keep the launch read path bounded, cache shared representations under stable identities, deliver immutable artifacts through the artifact path, assign cohorts deterministically, and move reporting away from the critical path.

The result is not merely higher throughput. It is a release system whose decisions remain explainable when the fleet contains many runtimes, channels, rollout states, network conditions, and artifact histories. That predictability is what lets teams increase traffic without turning every update into a new distributed-systems experiment.

## Common questions

### Should every React Native app use a CDN for OTA updates?

Large immutable artifacts are a natural CDN workload, while release selection may still require controlled or personalized logic. The important design is to separate cacheable shared state and artifacts from decisions that genuinely vary per installation.

### What should be in an OTA cache key?

Only inputs that change the representation or release decision. Project, platform, runtime, and channel are common stable dimensions. Installation IDs, credentials, and arbitrary user properties should not be added to a shared key unless the response is explicitly private and the cache behavior is designed for them.

### How do percentage rollouts work without storing every assignment?

A deterministic hash can map a stable installation and release identity to a bucket. The rollout percentage includes a range of buckets, so the same installation receives the same answer and expansion can remain monotonic.

### Is a downloaded OTA update the same as an adopted update?

No. A download may remain staged until a later launch, fail verification, or be superseded before activation. Adoption should describe the artifact actually executing and, ideally, whether it crossed the application's health boundary.

### Can analytics be eventually consistent?

Yes, if release selection and local safety do not depend on a synchronous analytics write. Install receipts should be idempotent, delays should be observable, and operators should know which decisions require fresh authoritative state rather than aggregated telemetry.
