> For the complete documentation index, see [llms.txt](https://mercure-technologies.gitbook.io/xprem/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mercure-technologies.gitbook.io/xprem/features/bundle-diffing-bsdiff.md).

# Bundle Diffing (bsdiff)

### Bundle diffing and bsdiff

Bundle diffing computes a patch between two updates, so that devices do not download the whole Hermes bundle again on every update. The patch is computed with the [bsdiff](https://www.daemonology.net/bsdiff/) algorithm and is meant to be much smaller than the bundle, which shortens update times and cuts egress.

Bundle diffing comes with trade-offs:

1. **A patch is not always worth it.** Some updates change the bytecode so much that serving the full bundle is cheaper than serving the patch.
2. **Computing a patch costs memory.** A job holds both bundles, the diff buffers and a verification copy at the same time, about six times the bundle size at peak.
3. **Patch size is not proportional to the lines you changed.** It can be counterintuitive, but a change that looks tiny in JSX/TSX can move a lot of bytecode. Read [how bsdiff works for OTA updates](https://xprem.dev/blog/how-bsdiff-works-ota-updates) to understand why.
4. **expo-updates requires two response headers on a patch**, `im: bsdiff` and `expo-base-update-id`. CDNs and signed bucket URLs do not send them by default, which changes how patches are served. See [#cdn-and-patch-serving](#cdn-and-patch-serving "mention")

### Enable bundle diffing on the server

Bundle diffing is off by default. Set `BUNDLE_DIFFING=true` to enable it.

{% hint style="info" %}
Bundle diffing is not available in [stateless mode](/xprem/3-0-x/controle-plane-mode/overview.md). It needs the control plane.
{% endhint %}

#### Patch computing

When an update is published, the server takes the 5 previous updates of the same branch, runtime version and platform, and enqueues one background job per update.

Each job:

* loads both bundles from the bucket
* computes the bsdiff patch
* rebuilds the new bundle from the patch and checks it byte for byte
* uploads the patch to the bucket under `/{keyPrefix}/{appId}/bsdiff/{branch}/{targetUpdateUUID}/{sourceUpdateUUID}`

**When a patch is not stored:**

* A patch is stored only if it is at most 30% of the gzipped size of the full bundle. This threshold is `BUNDLE_DIFFING_PATCH_MAX_RATIO`, a number between 0 and 1, default 0.3.
* One of the two updates was published before v3.2.0. That version introduced content-addressable storage, which changed how assets are stored and retrieved in the bucket, and patches need it.
* The two bundles are identical.

#### Protecting the server

bsdiff is memory hungry. To rule out any OOM during computation, bundles larger than a limit are rejected before being loaded. The limit is 64 MB by default and can be changed with `BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB`. Two jobs run at a time, so give the server about twelve times that limit in memory headroom.

#### CDN and patch serving

As said above, the expo-updates protocol requires a patch to be served with these response headers:

```
im: bsdiff
expo-base-update-id: {sourceUpdateUUID}
```

CDNs such as CloudFront or Cloudflare, and the bucket itself behind a signed URL, do not send them without configuration.

That is why the server serves patches itself by default, even when a CDN serves the other assets. To redirect patch requests to your CDN like any other asset, set:

`BUNDLE_DIFFING_CDN_REDIRECT=true`

The flag only does something with [CloudFront](/xprem/cdn/cloudfront.md) or a [generic CDN](/xprem/cdn/generic-cdn.md). With signed bucket URLs and no CDN, the server keeps serving patches itself.

{% hint style="danger" %}
Do not enable this unless your CDN is configured to return `im` and `expo-base-update-id` with the patch. Without them the device cannot apply the patch and does not fall back to the full bundle: the update fails on every device that has a patch available, until the flag is turned off.
{% endhint %}

**Configuring your CDN**

Whatever the provider, the rule is the same. For every response whose path contains `/bsdiff/`, add two headers: `im` with the value `bsdiff`, and `expo-base-update-id` with the last segment of the path, which is the UUID of the update the patch applies to. The path of a patch is:

```
/{keyPrefix}/{appId}/bsdiff/{branch}/{targetUpdateUUID}/{sourceUpdateUUID}
```

Patches are immutable, so your CDN can cache them like any other asset.

{% tabs %}
{% tab title="CloudFront" %}
The headers are added by a CloudFront Function on the viewer response event.

1. Open [CloudFront functions](https://console.aws.amazon.com/cloudfront/v4/home#/functions) and select **Create function**. Name it, keep the runtime **cloudfront-js-2.0**, select **Continue**.
2. In the **Build** tab, replace the sample code with this one and select **Save changes**:

   ```javascript
   function handler(event) {
     var response = event.response;
     var parts = event.request.uri.split('/');
     var i = parts.indexOf('bsdiff');
     if (response.statusCode === 200 && i !== -1 && parts.length === i + 4) {
       response.headers['im'] = { value: 'bsdiff' };
       response.headers['expo-base-update-id'] = { value: parts[i + 3] };
     }
     return response;
   }
   ```
3. In the **Test** tab, set **Event type** to **Viewer response**, set the request URI to `/app/bsdiff/main/target-uuid/source-uuid`, select **Test function**. The output must list `im` and `expo-base-update-id` in the response headers.
4. In the **Publish** tab, select **Publish function**, then **Add association**: your distribution, event type **Viewer response**, cache behavior **Default (\*)**.
5. Wait until the distribution is no longer **Deploying**.
   {% endtab %}

{% tab title="Cloudflare" %}
With a Worker:

1. Open **Workers & Pages**, select **Create**, start from the Hello World Worker, name it, select **Deploy**.
2. Select **Edit code**, replace the file with this one, select **Deploy**:

   ```javascript
   export default {
     async fetch(request) {
       const response = await fetch(request);
       const match = new URL(request.url).pathname.match(/\/bsdiff\/[^/]+\/[^/]+\/([^/]+)$/);
       if (!match || response.status !== 200) return response;
       const patched = new Response(response.body, response);
       patched.headers.set('im', 'bsdiff');
       patched.headers.set('expo-base-update-id', match[1]);
       return patched;
     },
   };
   ```
3. In the Worker's **Settings**, open **Domains & Routes**, select **Add**, then **Route**. Pick the zone of your CDN hostname and enter `cdn.example.com/*`.

Without code, on Business and Enterprise plans (regular expressions in rules are not available below):

1. Open the zone, then **Rules**, **Overview**, **Create rule**, **Response Header Transform Rule**.
2. Filter: **URI Path** **contains** `/bsdiff/`.
3. **Set static**: header `im`, value `bsdiff`.
4. **Set new header**, **Set dynamic**: header `expo-base-update-id`, expression `regex_replace(http.request.uri.path, "^.*/([^/]+)$", "${1}")`.
5. Select **Deploy**.
   {% endtab %}

{% tab title="Fastly" %}

1. Open the service, select **Edit configuration**, clone the active version.
2. Open the **VCL** tab, then **VCL snippets**, select **Add snippet**.
3. Type **Regular**, placement **within subroutine**, subroutine **deliver**. Paste this and select **Add**:

   ```vcl
   if (resp.status == 200 && req.url.path ~ "/bsdiff/[^/]+/[^/]+/([^/]+)$") {
     set resp.http.im = "bsdiff";
     set resp.http.expo-base-update-id = re.group.1;
   }
   ```
4. Select **Activate**, then **Activate on Production**.
   {% endtab %}

{% tab title="Azure Front Door" %}
Front Door Standard and Premium have a rules engine that can read a path segment with the server variable `{url_path:seg#}`, so no code is needed. Segments are numbered from 0 after the hostname. Without a key prefix, a patch path `/{appId}/bsdiff/{branch}/{target}/{source}` puts the source UUID in segment 4. With a key prefix, add one per prefix segment: under `ota/`, it is segment 5.

In the portal, open your Front Door profile, then **Rule sets**, add a rule set and a rule:

1. **Condition**: Request URL path, operator *Contains*, value `/bsdiff/`.
2. **Action**: Modify response header, operator *Overwrite*, header name `im`, value `bsdiff`.
3. **Action**: Modify response header, operator *Overwrite*, header name `expo-base-update-id`, value `{url_path:seg4}`.

Save, then associate the rule set with the route that serves your bucket.

The same with the Azure CLI:

```bash
az afd rule create \
  --resource-group <rg> --profile-name <profile> --rule-set-name bsdiff \
  --rule-name patchHeaders --order 1 \
  --match-variable UrlPath --operator Contains --match-values "/bsdiff/" \
  --action-name ModifyResponseHeader --header-action Overwrite \
  --header-name im --header-value bsdiff

az afd rule action add \
  --resource-group <rg> --profile-name <profile> --rule-set-name bsdiff \
  --rule-name patchHeaders \
  --action-name ModifyResponseHeader --header-action Overwrite \
  --header-name expo-base-update-id --header-value "{url_path:seg4}"

az afd route update \
  --resource-group <rg> --profile-name <profile> --endpoint-name <endpoint> \
  --route-name <route> --rule-sets bsdiff
```

{% endtab %}

{% tab title="Other providers" %}
Akamai EdgeWorkers, Bunny Edge Scripting and similar edge runtimes can add the two headers the same way: match `/bsdiff/` in the path and copy its last segment. Google Cloud CDN only supports static custom response headers, so keep the default and let the server serve patches.
{% endtab %}
{% endtabs %}

**Check before enabling the flag**

Take the key of a stored patch from the bucket:

```bash
aws s3 ls s3://YOUR_BUCKET/ --recursive | grep /bsdiff/
```

Request it through the CDN:

```bash
curl -sI "https://cdn.example.com/{appId}/bsdiff/{branch}/{targetUpdateUUID}/{sourceUpdateUUID}"
```

On CloudFront, sign the URL first and curl the one it prints:

```bash
aws cloudfront sign \
  --url "https://YOUR_CLOUDFRONT_DOMAIN/{appId}/bsdiff/{branch}/{targetUpdateUUID}/{sourceUpdateUUID}" \
  --key-pair-id YOUR_KEY_PAIR_ID \
  --private-key file://private_key.pem \
  --date-less-than 2030-01-01
```

Both headers must be in the response (CloudFront capitalizes them as `Im` and `Expo-Base-Update-Id`):

```
HTTP/2 200
im: bsdiff
expo-base-update-id: {sourceUpdateUUID}
```

Then set `BUNDLE_DIFFING_CDN_REDIRECT=true`, restart, and run expo-updates-inspector as described below: the launch asset must redirect to the CDN, with both headers on the last hop.

#### Debugging patches

A device that cannot use a patch falls back to the full bundle and says nothing about it. To see what your server really answers, run [expo-updates-inspector](https://github.com/axelmarciano/expo-updates-inspector):

```bash
npx expo-updates-inspector
```

It makes the same requests an Expo app makes and shows everything that comes back: the manifest, the response headers, every redirect hop and every asset with its hash verified. For patches, it asks for the launch asset with `A-IM: bsdiff`, runs the checks the native client runs before applying a patch, and measures what the patch saves.

The dashboard shows the server side of the same story: open an update to see the patches computed toward it, their status and size, and recompute them if needed:

<figure><img src="https://2155714808-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Frv7YwbmypjuLMR4sh6Qt%2Fuploads%2FTnquvjC7xZrDICL4BqR3%2Fimage.png?alt=media&#x26;token=ae770512-aca3-4606-9453-3352f9303ef3" alt=""><figcaption></figcaption></figure>

### Enable bundle diffing in your Expo application

The client side is expo-updates (supported from Expo SDK 55). Versions 56.0.13 and later accept patches by default. On SDK 55, enable it in the `updates` section of `app.config.(ts|js)`:

```json
"updates": {
  ...,
  "enableBsdiffPatchSupport": true
}
```

This is a native build setting, so it takes a rebuild and a release on the App Store and Play Store. You can enable bundle diffing on the server before that: the server keeps serving full bundles to apps that do not ask for patches.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mercure-technologies.gitbook.io/xprem/features/bundle-diffing-bsdiff.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
