Bundle Diffing
Ship smaller OTA updates by reusing unchanged bundle files.
What it is
Bundle diffing reuses files already installed on the device to reduce how much of the next OTA bundle it downloads. An OTA bundle is a folder of files.
First OTA update:
built-in unchanged files
+ downloaded changed files
-> install OTA bundle
Later OTA update:
local unchanged files
+ downloaded changed files
+ optional Hermes patch
-> next OTA bundleWhat the device downloads depends on what changed:
| When this happens | What the device downloads |
|---|---|
| First OTA after native install | Reuse matching built-in files and download the rest |
| Later OTA, unchanged file | Nothing; the file is copied locally |
| Later OTA, changed regular asset | Complete new asset file |
| Later OTA, changed Hermes bundle file | .bsdiff patch when available, otherwise the complete new Hermes bundle file |
These are the per-file transfer rules. After local reuse, the runtime can
instead download the automatically generated bundle.tar.br archive when at
least two files remain and its transfer-cost comparison favors the archive; see
Per-file or archive.
For example, an update with a 10 MB Hermes file might only need to download a 600 KB patch when the byte-level changes are small.
Patch size depends on the changes between two OTA bundles. Large changes can produce a patch larger than the complete Hermes file; the server then omits the patch and the client downloads the complete file.
You do not need to change your update-check code or deploy command. After your app is using a diff-enabled Hot Updater runtime, new deployments include the data needed for bundle diffing automatically.
Two layers
Bundle diffing has two separate optimizations:
- Files that already exist locally can be copied into the next OTA bundle. This applies to regular assets and the Hermes bundle file.
- If the Hermes bundle changed and Hot Updater has a matching
.bsdiffpatch, the app can download the patch instead of the complete new Hermes bundle file.
Assets are reused at the file level. Changed Hermes bundles can also be patched at the byte level.
| File type | If unchanged | If changed |
|---|---|---|
| Regular asset | Copy local file | Download the complete new asset file |
| Hermes bundle file | Copy local file | Apply .bsdiff patch, then fallback to the complete new Hermes bundle file |
Regular assets include files such as images and fonts. They are not binary
patched; if they changed, the complete new asset file is downloaded. The Hermes
bundle is the only file type that gets the extra .bsdiff patch optimization.
How the server describes an update
The artifact endpoint returns the target manifest and, for the Hermes bundle, can include a patch from the current OTA bundle. When both sizes are known, the server includes the patch only when it is strictly smaller than the original.
downloadByteSize in a manifest describes the representation that the device
actually downloads. For a Brotli asset, that means the compressed payload, not
the uncompressed installed file. Its optional downloadFileHash is the
SHA-256 of those exact served bytes and identifies their content-addressed
object; the existing fileHash still identifies and verifies the logical
installed file.
Raw assets use the logical fileHash key and do not need downloadFileHash.
The client always receives the original descriptor, so a missing, corrupt, or
inapplicable patch falls back to the complete file.
How to use it
No special command is required:
npx hot-updater deploy -p ios
npx hot-updater deploy -p androidFor apps running a bundle-diffing capable runtime, that is enough to enable manifest-based diffing.
A diff-enabled runtime means the native app was built with a Hot Updater version that includes bundle diffing support.
Hermes binary patch optimization is also enabled by default. Keep your existing build, storage, database, and plugins settings when changing this option:
import { defineConfig } from "hot-updater";
export default defineConfig({
patch: {
enabled: true,
maxBaseBundles: 3,
},
});This is the default behavior. Add the patch block only when you want to
override it, for example to disable binary patch generation or change how many
previous OTA bundles are used as patch bases.
| Option | Default | Meaning |
|---|---|---|
patch.enabled | true | Generates .bsdiff patches for changed Hermes bundles. |
patch.maxBaseBundles | 3 | Uses up to this many previous compatible OTA bundles as patch bases. |
Binary patches
Binary patches are an extra optimization for Hermes bundle files:
- During deploy, Hot Updater creates a
.bsdiffpatch between a previous Hermes bundle and the new Hermes bundle. - At runtime, the app downloads that patch and applies it to the
already-installed Hermes bundle with the
bspatchalgorithm. - The result is the new Hermes bundle file, without downloading that complete file.
This does not replace manifest-based file reuse. Regular assets are still copied from the current OTA bundle or downloaded as complete new files.
maxBaseBundles controls how many previous compatible OTA bundles are used as
patch bases when deploying a new OTA update.
For example:
Base bundle -> OTA A -> OTA B -> OTA CWhen deploying OTA C with maxBaseBundles: 2, Hot Updater can prepare patches
from:
OTA BtoOTA COTA AtoOTA C
This means devices currently on OTA A or OTA B can download a small patch to
OTA C instead of the complete new Hermes bundle file. A device on the built-in
bundle reuses matching packaged files and downloads complete files for the rest.
Devices on an older OTA outside the selected patch bases download the complete
new Hermes bundle file.
Notes:
- Defaults to
3. - Compatible patch bases are artifacts used by older enabled Bundles in the same channel and platform.
- Hot Updater also requires the same fingerprint, or overlapping target app version ranges when using the App Version strategy.
- Higher values increase the range of OTA versions covered by patches. Deploy creates at most one patch per selected base, so patch-generation time and storage work grow roughly with the number of selected bases rather than exponentially.
If a patch cannot be created, Hot Updater still deploys the normal OTA update.
How it works
Create a patch for existing artifacts
Deploy creates patches automatically. To add one for artifacts that already
exist, obtain their Artifact IDs from Console Advanced diagnostics or the
bundle_id field in bundle show <public-id> --json, then run:
npx hot-updater patch -p ios --artifact-id <target-artifact-id> --base-artifact-id <base-artifact-id>Use the artifacts' platform (ios or android) and add -c <channel> if that
channel selects a different CLI configuration. The target is the newer artifact;
the base is the OTA artifact installed on devices that should use the patch.
Public Bundle IDs from deploy or promote are delivery
policy IDs and must not be used as Artifact IDs. The patch is recorded in the
target artifact's patches relation; the complete target file remains
available as the fallback.
Both artifacts must have readable manifests with a changed Hermes bundle at
the same manifest path. Missing, unchanged or mismatched Hermes inputs cannot
produce a patch; ordinary assets use file reuse or full-file downloads.
During deploy
Hot Updater prepares the manifest delivery path:
- Uploads the manifest and per-file manifest assets.
- Records manifest and asset storage metadata.
- Creates
bundle.tar.brand records its hash and sizes in the manifest. - Creates
.bsdiffpatch files for changed Hermes bundles when binary patching is enabled.
First OTA update
There is no older OTA bundle on the device yet. Hot Updater resolves each manifest path against the packaged app, verifies the packaged file's hash, and copies matches into staging. It downloads the remaining files individually or uses the archive when the transfer-cost comparison selects it. Hermes patching still starts with a later OTA because a patch from the built-in Hermes file is not generated yet.
Later OTA updates
On the per-file route, later updates can be assembled from smaller pieces:
- The app reads the new update manifest.
- Files that already exist in the current OTA bundle are copied locally.
- Files that are new or changed are downloaded.
- For the Hermes bundle, the app first checks whether there is a
.bsdiffpatch that matches the currently installed OTA bundle. - If the patch matches, the app downloads the patch and applies it to the existing Hermes bundle to produce the new Hermes bundle.
Per-file or archive
Deploy compresses eligible individual files with Brotli and also creates
bundle.tar.br with every target file. The manifest records each file's
installed hash and, for a compressed file, its download hash and size, plus
the archive's hash and sizes. There is no setting for compression or the
archive, and ZIP and tar.gz archives are not supported.
With at least two files left to download, the client compares the archive's bytes with the per-file transfer, patches included. When every target file needs downloading and no patch is offered, it also allows the archive's TAR framing and padding, which saves many requests for a complete bundle whose files compress poorly. Otherwise the archive must be smaller. Missing or invalid sizes keep the per-file route.
Both routes verify the same files and hashes before activating the update. If the archive fails, the client discards its extraction and tries the per-file plan once, which can transfer more bytes.
Verification and fallback
Before using a patched Hermes bundle, the app verifies:
- the base file hash
- the patch file hash
- the final output hash
If anything does not match, or if the patch cannot be applied, Hot Updater downloads and verifies the complete new Hermes bundle file.
Inspect downloaded files
Use useHotUpdaterStore when you want to check which files were downloaded
during a bundle diffing update.
Use progress for the overall update progress. Manifest updates use
artifactType: "diff"; details.files describes their download entries.
completedFilesCount and totalFilesCount count those entries, not the number
of changed or installed target files.
import { Text, View } from "react-native";
import { useHotUpdaterStore } from "@hot-updater/react-native";
function UpdateDownloadProgress() {
const { artifactType, details, progress } = useHotUpdaterStore();
const files = artifactType === "diff" ? (details?.files ?? []) : [];
return (
<View>
<Text>Overall progress: {Math.round(progress * 100)}%</Text>
{artifactType === "diff" && details ? (
<View>
<Text>
Downloads: {details.completedFilesCount}/
{details.totalFilesCount}
</Text>
{files.map((file) => (
<View key={file.path}>
<Text>{file.path}</Text>
<Text>
{file.status} · {Math.round(file.progress * 100)}%
</Text>
{file.downloadPath !== file.path ? (
<Text>Downloaded artifact: {file.downloadPath}</Text>
) : null}
</View>
))}
</View>
) : null}
</View>
);
}For individual-file downloads, path is the final file path inside the OTA
bundle and downloadPath is the transferred artifact. They can differ for an
intermediate artifact such as a .bsdiff patch.
For an archive download, details.files contains one entry with both path
and downloadPath set to bundle.tar.br, and totalFilesCount is 1. That
entry represents the transport archive, not a file in the installed bundle,
even when the archive installs many target files.
Signing
Bundle diffing works without signing, but signing is recommended. When signing is enabled, the manifest and manifest assets are verified before installation.
See Bundle Signing for setup.