HotupdaterHot Updater
Deliver updates

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 bundle

What the device downloads depends on what changed:

When this happensWhat the device downloads
First OTA after native installReuse matching built-in files and download the rest
Later OTA, unchanged fileNothing; the file is copied locally
Later OTA, changed regular assetComplete 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:

  1. Files that already exist locally can be copied into the next OTA bundle. This applies to regular assets and the Hermes bundle file.
  2. If the Hermes bundle changed and Hot Updater has a matching .bsdiff patch, 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 typeIf unchangedIf changed
Regular assetCopy local fileDownload the complete new asset file
Hermes bundle fileCopy local fileApply .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 android

For 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:

hot-updater.config.ts (partial)
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.

OptionDefaultMeaning
patch.enabledtrueGenerates .bsdiff patches for changed Hermes bundles.
patch.maxBaseBundles3Uses 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 .bsdiff patch 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 bspatch algorithm.
  • 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 C

When deploying OTA C with maxBaseBundles: 2, Hot Updater can prepare patches from:

  • OTA B to OTA C
  • OTA A to OTA 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.br and records its hash and sizes in the manifest.
  • Creates .bsdiff patch 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:

  1. The app reads the new update manifest.
  2. Files that already exist in the current OTA bundle are copied locally.
  3. Files that are new or changed are downloaded.
  4. For the Hermes bundle, the app first checks whether there is a .bsdiff patch that matches the currently installed OTA bundle.
  5. 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.

On this page