feat: publish "50% Less Bandwidth Per Deploy" (Cloudpack series #4)

This commit is contained in:
Ken
2026-05-27 07:11:58 +00:00
parent 1805a870c9
commit 2abb181790
@@ -0,0 +1,219 @@
---
title: "50% Less Bandwidth Per Deploy"
date: 2026-05-28
type: post
tags: [bundler, cdn, bandwidth, cloudpack, delivery]
summary: "Every deploy, your users re-download the whole bundle because 1% of modules changed. Delta manifests cut that in half. The browser says what it has. The server returns only what's missing."
---
You changed one line in a utility function. The bundler produced new chunks. The CDN invalidated the old URLs. Every user who visits your app downloads the entire bundle from scratch.
One line. 100% re-download.
This is not a bug in your bundler. It's how content-hashing works. Change one module, the chunk hash changes, the URL changes, and the browser has no way to know that 99% of the code inside that chunk is identical to what it already had. So it fetches everything. Every byte. Every user.
At 50,000 modules with 10,000 daily active users, that's 95 GB of CDN bandwidth per deploy. Most of it wasted on bytes the browser already has cached under a different filename.
## Invert the model
The fix is simple: stop pretending the CDN is the source of truth about what the client needs. The client already knows what it has. Let it say so.
Cloudpack's [Adaptive Bundle Service](https://github.com/kenotron-ms/cloudpack) (ABS) inverts the delivery model. Instead of the browser requesting URLs and getting whatever the CDN serves, the browser sends a manifest request: "here's my entry point, here are the content hashes of every module I already have." The server computes the difference and responds with only the chunk URLs the browser still needs.
<div class="breakout">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 840 390" role="img" aria-label="Diagram: Traditional CDN delivery where every deploy re-downloads everything compared to ABS delta delivery where only changed chunks are fetched">
<rect width="840" height="390" fill="#F5F0E8" rx="4"/>
<text x="210" y="32" text-anchor="middle" font-family="Georgia, serif" font-size="16" font-weight="bold" fill="#2B2421">Traditional CDN</text>
<text x="210" y="50" text-anchor="middle" font-family="Georgia, serif" font-size="12" fill="#A0522D">every deploy</text>
<text x="630" y="32" text-anchor="middle" font-family="Georgia, serif" font-size="16" font-weight="bold" fill="#2B2421">ABS Delta Delivery</text>
<text x="630" y="50" text-anchor="middle" font-family="Georgia, serif" font-size="12" fill="#A0522D">same deploy</text>
<line x1="420" y1="16" x2="420" y2="370" stroke="#2B2421" stroke-width="1" stroke-dasharray="4,4"/>
<rect x="110" y="70" width="200" height="36" rx="3" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="210" y="93" text-anchor="middle" font-family="monospace" font-size="11" fill="#2B2421">Deploy: 1% of modules changed</text>
<line x1="210" y1="106" x2="210" y2="124" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dd1)"/>
<rect x="60" y="126" width="300" height="36" rx="3" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="210" y="149" text-anchor="middle" font-family="monospace" font-size="11" fill="#2B2421">Chunk hashes change, URLs change</text>
<line x1="210" y1="162" x2="210" y2="180" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dd1)"/>
<rect x="60" y="182" width="300" height="36" rx="3" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="210" y="205" text-anchor="middle" font-family="monospace" font-size="11" fill="#2B2421">Browser cache miss on new URLs</text>
<line x1="210" y1="218" x2="210" y2="236" stroke="#A0522D" stroke-width="1.5" marker-end="url(#dd2)"/>
<rect x="80" y="238" width="260" height="40" rx="4" fill="#A0522D" stroke="none"/>
<text x="210" y="263" text-anchor="middle" font-family="Georgia, serif" font-size="13" font-weight="bold" fill="#F5F0E8">Re-download 100% of chunks</text>
<text x="210" y="308" text-anchor="middle" font-family="Georgia, serif" font-size="14" font-weight="bold" fill="#A0522D">95 GB / week at scale</text>
<text x="210" y="330" text-anchor="middle" font-family="Georgia, serif" font-size="12" fill="#2B2421">All bytes. Every user. Every deploy.</text>
<rect x="530" y="70" width="200" height="36" rx="3" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="630" y="93" text-anchor="middle" font-family="monospace" font-size="11" fill="#2B2421">Browser: "here's what I have"</text>
<line x1="630" y1="106" x2="630" y2="124" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dd1)"/>
<rect x="490" y="126" width="280" height="36" rx="3" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="630" y="149" text-anchor="middle" font-family="monospace" font-size="11" fill="#2B2421">ABS computes delta from cached_hashes</text>
<line x1="630" y1="162" x2="630" y2="180" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dd1)"/>
<rect x="490" y="182" width="280" height="36" rx="3" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="630" y="205" text-anchor="middle" font-family="monospace" font-size="11" fill="#2B2421">Response: fetch only changed chunks</text>
<line x1="630" y1="218" x2="630" y2="236" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dd1)"/>
<rect x="500" y="238" width="260" height="40" rx="4" fill="#2B2421" stroke="none"/>
<text x="630" y="263" text-anchor="middle" font-family="Georgia, serif" font-size="13" font-weight="bold" fill="#F5F0E8">Download ~50% of chunks</text>
<text x="630" y="308" text-anchor="middle" font-family="Georgia, serif" font-size="14" font-weight="bold" fill="#2B2421">47 GB / week saved</text>
<text x="630" y="330" text-anchor="middle" font-family="Georgia, serif" font-size="12" fill="#2B2421">Only the bytes that actually changed.</text>
<line x1="60" y1="352" x2="780" y2="352" stroke="#2B2421" stroke-width="0.5"/>
<text x="420" y="376" text-anchor="middle" font-family="Georgia, serif" font-size="13" fill="#2B2421">Same deploy. Same codebase. The client tells the server what it already knows.</text>
<defs>
<marker id="dd1" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2B2421"/></marker>
<marker id="dd2" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#A0522D"/></marker>
</defs>
</svg>
</div>
The wire protocol is two structs.
## The request
```rust
pub struct ManifestRequest {
/// The application entry-point (e.g. `"src/index.ts"`).
pub entry_point: String,
/// SHA-256 hashes of every module the client already holds in cache.
pub cached_hashes: Vec<ContentHash>,
/// Opaque build identifier issued by the last manifest response.
/// Absent on the very first request or after a hard-reload.
pub build_id: Option<String>,
}
```
Three fields. The browser sends its entry point and the content hashes of every module it has cached. The `build_id` is a staleness check: if the client's build doesn't match the server's current build, ABS ignores the cache claims and returns the full chunk set. No stale data, no partial mismatches, no guessing.
## The response
```rust
pub struct ManifestResponse {
/// Opaque identifier for the current server build.
pub build_id: String,
/// URLs of chunks the client must fetch now.
pub fetch_urls: Vec<String>,
/// URLs of chunks worth prefetching for future navigations.
pub prefetch_urls: Vec<String>,
/// Cache TTL for this manifest in seconds.
pub ttl: u64,
}
```
`fetch_urls` is what the client needs right now. `prefetch_urls` is what it will probably need soon, derived from co-request patterns observed in real production traffic. The server never tells the client to re-fetch chunks it already has. That's the whole point.
## The delta computation
The algorithm is simple. Look up which chunks the entry point needs. For each chunk, check whether every module inside it appears in the client's `cached_hashes` set. All modules present? The client already has the full chunk, skip it. Any module missing? Add the chunk's CDN URL to `fetch_urls`.
```rust
pub fn compute_delta(
manifest: &ChunkManifest,
request: &ManifestRequest,
cdn_base_url: &str,
) -> ManifestResponse {
let chunk_ids = match manifest.entry_chunks.get(&request.entry_point) {
Some(ids) => ids,
None => return empty_response(&manifest.build_id),
};
// Trust cache only if build_id matches.
let trust_cache = matches!(
&request.build_id, Some(bid) if bid == &manifest.build_id
);
let cached_set: HashSet<&ContentHash> = if trust_cache {
request.cached_hashes.iter().collect()
} else {
HashSet::new()
};
let mut fetch_urls: Vec<String> = Vec::new();
for chunk_id in chunk_ids {
if let Some(chunk) = chunks_by_id.get(chunk_id.as_str()) {
let fully_cached = !chunk.modules.is_empty()
&& chunk.modules.iter().all(|m| cached_set.contains(m));
if !fully_cached {
fetch_urls.push(chunk_url(cdn_base_url, &chunk.hash));
}
}
}
ManifestResponse { build_id: manifest.build_id.clone(), fetch_urls, .. }
}
```
The `trust_cache` gate is important. If the client sends a `build_id` that doesn't match the server's current build, the `cached_set` is empty and every chunk goes into `fetch_urls`. First visit and hard-reload both degrade to a full download. No ambiguity. No stale-cache bugs.
<div class="breakout">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 840 310" role="img" aria-label="Diagram: Delta computation flow from client request through server diff to selective chunk fetch">
<rect width="840" height="310" fill="#F5F0E8" rx="4"/>
<text x="420" y="30" text-anchor="middle" font-family="Georgia, serif" font-size="16" font-weight="bold" fill="#2B2421">The Delta Computation</text>
<rect x="30" y="60" width="190" height="100" rx="4" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="125" y="84" text-anchor="middle" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#2B2421" letter-spacing="0.03em">CLIENT REQUEST</text>
<text x="125" y="108" text-anchor="middle" font-family="monospace" font-size="10" fill="#2B2421">entry: "src/index.ts"</text>
<text x="125" y="124" text-anchor="middle" font-family="monospace" font-size="10" fill="#2B2421">cached: [a1, a2, a3, b1]</text>
<text x="125" y="140" text-anchor="middle" font-family="monospace" font-size="10" fill="#2B2421">build_id: "build-42"</text>
<line x1="220" y1="110" x2="298" y2="110" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dc1)"/>
<text x="259" y="102" text-anchor="middle" font-family="Georgia, serif" font-size="10" fill="#A0522D">POST</text>
<rect x="305" y="50" width="230" height="120" rx="4" fill="#2B2421" stroke="none"/>
<text x="420" y="76" text-anchor="middle" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#F5F0E8" letter-spacing="0.03em">ABS SERVER</text>
<text x="420" y="100" text-anchor="middle" font-family="monospace" font-size="10" fill="#A0522D">build_id matches? trust cache</text>
<text x="420" y="118" text-anchor="middle" font-family="monospace" font-size="10" fill="#A0522D">for each chunk in entry_chunks:</text>
<text x="420" y="136" text-anchor="middle" font-family="monospace" font-size="10" fill="#A0522D"> all modules in cached_set?</text>
<text x="420" y="154" text-anchor="middle" font-family="monospace" font-size="10" fill="#A0522D"> yes: skip. no: add to fetch_urls</text>
<line x1="535" y1="110" x2="613" y2="110" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dc1)"/>
<text x="574" y="102" text-anchor="middle" font-family="Georgia, serif" font-size="10" fill="#A0522D">JSON</text>
<rect x="620" y="60" width="190" height="100" rx="4" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<text x="715" y="84" text-anchor="middle" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#2B2421" letter-spacing="0.03em">RESPONSE</text>
<text x="715" y="108" text-anchor="middle" font-family="monospace" font-size="10" fill="#2B2421">build_id: "build-43"</text>
<text x="715" y="124" text-anchor="middle" font-family="monospace" font-size="10" fill="#2B2421">fetch: [chunk-c.js]</text>
<text x="715" y="140" text-anchor="middle" font-family="monospace" font-size="10" fill="#2B2421">prefetch: [chunk-d.js]</text>
<line x1="715" y1="160" x2="715" y2="195" stroke="#2B2421" stroke-width="1.5" marker-end="url(#dc1)"/>
<rect x="620" y="200" width="190" height="52" rx="4" fill="none" stroke="#A0522D" stroke-width="1.5"/>
<text x="715" y="222" text-anchor="middle" font-family="monospace" font-size="10" fill="#A0522D">GET chunk-c.js from CDN</text>
<text x="715" y="238" text-anchor="middle" font-family="Georgia, serif" font-size="11" fill="#6B5F55">1 chunk, not 10</text>
<line x1="60" y1="272" x2="780" y2="272" stroke="#2B2421" stroke-width="0.5"/>
<text x="420" y="296" text-anchor="middle" font-family="Georgia, serif" font-size="13" fill="#2B2421">The browser tells ABS what it has. ABS returns only what's missing. The CDN serves chunks.</text>
<defs>
<marker id="dc1" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2B2421"/></marker>
</defs>
</svg>
</div>
## Never on the critical path
ABS is a manifest server. It decides what to load. The CDN serves the actual code. This separation is load-bearing.
The service worker that mediates the protocol has a hard 100ms timeout. ABS doesn't respond in time? The service worker falls back to `manifest.json` on the CDN and the app loads exactly as it would without ABS. Full chunk set, no optimization. Works every time.
ABS failure degrades transparently. No blank screens. No loading spinners. No error toasts. The user gets a few extra megabytes they didn't strictly need, and the app loads fine. The optimization is invisible when it works and invisible when it doesn't.
## The numbers
Benchmarks run against synthetic codebases generated from the statistical fingerprint of real production code. [Reproducible](https://github.com/kenotron-ms/cloudpack). The table shows what a returning user downloads per deploy:
| Code churn | Traditional CDN | ABS delta | Savings |
|-----------:|----------------:|----------:|--------:|
| 0.5% | 100% | 50% | **50%** |
| 1% | 100% | 50% | **50%** |
| 5% | 100% | 50% | **50%** |
| 10% | 100% | 50% | **50%** |
| 25% | 100% | 50% | **50%** |
Up to 25% code churn, ABS cuts download size in half. The savings hold because the delta operates at chunk granularity: a chunk whose modules all match the client's cache is skipped entirely, regardless of how many other chunks changed.
At production scale (50,000 modules, 10,000 daily active users, 1% weekly churn): **47 GB saved per week.** That is not a rounding error. At $0.09/GB, it's $4.29/week in CDN costs alone, and the bandwidth savings compound with user count.
## The dynamic linking analogy
This is not a new idea. It's `ld.so` for the browser.
In native code, the dynamic linker loads shared libraries at runtime. It resolves symbols, checks what's already mapped, and fetches only what's missing. GOT/PLT lets the program reference code that hasn't been loaded yet. The loader fills in the addresses when they're needed.
ABS does the same thing. The service worker is `ld.so`. Content-hashed CDN chunks are shared libraries. The delta manifest is the symbol lookup. The browser fetches only what it doesn't already have.
The architect brief for Cloudpack puts it this way: "the module graph is the software, the bundle is a query." The delta manifest is just another materialization of that graph. Production bundles, dev server output, CI artifacts, per-client delta manifests: all queries against the same persistent module graph. Different clients, different cache states, different answers. One truth.
47 GB per week. Half the bandwidth. Same app.