Measured, not projected

The benchmarks, in full.

Every number here is measured. Update payloads were captured over real HTTP sessions (cavs-server + cavs-client). Real-game figures come from open-source Godot projects exported to PCK at two real points in their git history. "Update" means what a player who already has the previous version downloads; the full-download baseline is the new release compressed with zstd -3.

butler v15.28.0 · xdelta3 3.2.0 · bsdiff 4.3 Apple M3 Pro, release binaries every output byte-identical (cmp / BLAKE3)
The strong case

Update payloads on real games.

A player who already has the previous version downloads only the missing chunks required to reconstruct the new release.

GameUpdateFull (zstd)CAVS (64 KiB)Saved
godotengine/tps-demo (569 MB)tag 4.5 → master, 7 files247.60 MiB1.64 MiB−99.3%
MechanicalFlower/Marble1.6.0 → 1.6.16.55 MiB0.14 MiB−97.9%
GDQuest 3D third-personHEAD~10 → HEAD, 468 files27.61 MiB8.70 MiB−68.5%

Re-downloading the same version costs 0 bytes of payload — everything resolves from the persistent cache as references. All reconstructions were SHA-256 verified and byte-identical; the Godot runtime mounted the reconstructed PCKs via load_resource_pack().

The dual route

Cold install is never worse
than a full compressed download.

cavs pack --bootstrap also emits the full artifact zstd-19-compressed. The server offers it to cold clients whenever it beats the chunk path, and the client seeds its chunk cache from it — so the next update is incremental with zero extra download. When the payload is incompressible, the server keeps the chunk path.

GamePCK v1Full zstd-3Dual route coldvs zstd-3
godotengine/tps-demo569.15 MiB247.62 MiB221.42 MiB−10.6%
GDQuest 3D third-person61.09 MiB27.66 MiB24.43 MiB−11.7%
MechanicalFlower/Marble9.59 MiB6.55 MiB5.68 MiB−13.2%
Godot 4.7 export suite5.39 MiB4.52 MiB4.20 MiB−7.1%

The server routed all four games to the bootstrap automatically (per-session estimate vs threshold); every reconstruction was byte-identical.

Update-check cost

Compact binary manifests.

When payload is already cached, the manifest becomes the cost of checking for updates — the "is there an update?" call every launcher makes. The binary manifest (CAVSMF2) stores each unique chunk hash once and references it by varint index.

GameJSON v1Binary v2Parse
tps-demo894.2 KiB208.5 KiB0.53 → 0.49 ms
GDQuest103.1 KiB24.8 KiB0.062 → 0.058 ms
Marble20.3 KiB5.1 KiB0.018 → 0.017 ms

Chunk-path bytes are unchanged; the win lands where metadata dominates — a warm re-fetch now costs ~75% less wire.

Positioning

CAVS vs dedicated delta tools.

Per-pair delta tools win on raw bytes for one exact version pair — that is inherent and expected. The point is the operational model: CAVS packages once per release and serves any version jump from one deduplicated, CDN-cacheable store, with resumable, self-repairing delivery.

Game (v1→v2)zstd fullrsync wirexdelta3 -9bsdiffCAVS (64k)
tps-demo247.60462.950.030.031.64
GDQuest27.6154.863.783.828.70
Marble6.550.020.000.000.14
On tps-demo, bsdiff cost 137 s and 9.1 GB RAM to build one patch — while CAVS packed once in 3.5 s, reusable across every client and version jump.
Auto-route optimized delivery

Every route for one update, measured.

The delivery planner prices chunks, hybrid reconstruction, offline plans, optimized sidecars, bootstrap, full download and no-op against client state, memory budget and measured cost. Below is the full external pipeline on the same 126 MiB directory release — butler's default diff and optimized rediff --rediff-quality 9, plus byte-level proxies. Every output byte-identical; RSS via /usr/bin/time on real binaries.

RouteDownloadGenerateApplyPeak RAM
CAVS auto-route (plan)2.51 MiB0.54 s391 ms23 MiB
butler diff (default)2.52 MiB1.0 s331 ms35 MiB
butler rediff q9 (optimized)2.51 MiB11.6 s403 ms97 MiB
pairwise proxy — xdelta3 + zstd-193.01 MiB4.1 s3.4 s397 MiB
pairwise proxy — bsdiff + zstd-193.02 MiB25.0 s3.4 s2.3 GiB
full download (zstd-19)62.12 MiB3.9 s14 ms
Bytes and apply time tie the optimized patch, but CAVS generates 21× faster and applies with 4.2× less memory (23 vs 97 MiB). On a shifted artifact (every byte moved) CAVS ships 4.21 KiB vs the optimized patch's 11.39 KiB — smaller, faster to apply, and a fraction of the RAM.

Single artifact & the compressed-blob case

Change (128 MiB artifact)full zstd-19CAVS planbutler optimizedbsdiff proxyxdelta3 proxy
small change (~3%)64.05 MiB1.94 MiB1.94 MiB1.96 MiB1.94 MiB
shifted (every byte moves)64.06 MiB4.21 KiB11.39 KiB4.59 KiB4.34 KiB
compressed blob (62 MiB .tar.zst)2.53 MiB2.90 MiB2.53 MiB

On the shifted artifact CAVS's content-defined blocks survive the unaligned insert and beat butler's fixed-block scan 16×. The compressed-blob weak case is closed: the per-file optimizer routes the high-entropy blob through a byte-level delta — 2.53 MiB where naïve block routes paid 21.9 MiB.

SteamPipe-style analysis — v0.9.0

Layout, not content,
decides update cost.

The v0.9.0 analyzer estimates a build transition under a public fixed-1MiB chunk model (SteamPipe-style — never Valve's implementation) and diagnoses the layouts that bloat updates. Below: the same 64 KiB of real change in a 32-asset pack, under different layouts. Deterministic datasets, reproducible with cavs bench steampipe-cases.

Pack layoutSteamPipe-style estimateFixed reuseCAVS .cavsplanDiagnosis
Localized edit, stable layout1.00 MiB97.0%131 KiBOK
4 KiB inserted at the front32.88 MiB0.0%7.4 KiBasset_shuffling
Same assets, shuffled order32.88 MiB0.0%67 KiBasset_shuffling
Per-asset headers rewritten each build32.00 MiB2.7%2.13 MiBtoc_churn
Same edit, TOC centralized (the fix)1.88 MiB94.3%132 KiBOK
Pack compressed as one streamwhole blob, 0% reuse0.0%130 KiBcompressed blob
Per-asset compression, padded slots (the fix)97 KiB75.0%68 KiBOK
A 4 KiB insert turns a 32.88 MiB pack into a full re-download under fixed chunks — the CAVS plan for the same change is 7.4 KiB. cavs analyze steampipe names the cause (asset_shuffling, toc_churn, …) and the fix; applying the fixes recovers 94% reuse under the same fixed-chunk model.

Download size is not update time — local disk I/O

~3-byte change in a 256 MiB packDownloadLocal read + writeHDD est.NVMe est.
One monolithic pack2.00 MiB512 MiB4.7 s176 ms
Split into 8 × 32 MiB (the fix)2.00 MiB128 MiB1.2 s45 ms
Raw full download (reference)256 MiB256 MiB2.6 s102 ms

On an HDD, the "smart" 2 MiB update of the monolithic pack is slower than the dumb 256 MiB full download — a fixed-chunk updater rebuilds every touched file in full. cavs io-estimate flags when local I/O dominates the network saving; splitting the pack fixes it at the same download size.

Depots, sharing and install plans

// depot analyze-sharing

98.9% shared

Windows ↔ Linux depots of the same build share 48.83 MiB of 49.11 MiB — the workspace prices exactly what a shared depot would save in storage and egress.

// install-plan

0 B for the demo owner

Install plans filter depots by platform, language and ownership and reuse chunks across depots: a demo owner who already installed the full game downloads nothing.

// plan-update

Honest routing

The policy planner picks the 129 KiB .cavsplan for every previous-install state — and honestly picks the raw full download for a cold install of incompressible data, because a bootstrap would save nothing.

Raw outputs and reproduction commands: docs/results/v0.9.0/ in the repository. Every SteamPipe-style figure is an estimate from public documentation, not Valve's exact SteamPipe implementation.

Reuse the installed build

The version on disk
is a source of bytes.

A player with the old build installed but an empty cache copies verified ranges straight from the old file and fetches only what changed — every range BLAKE3-checked before it is written, the final output gated by SHA-256. Measured on a 128 MiB synthetic build against a full re-download.

Update variantFull re-downloadCAVS — cold cache + old installReduction
small change64.55 MiB6.24 MiB−90.3%
medium change64.56 MiB26.53 MiB−58.9%
shifted (every byte moves)64.56 MiB10.9 KiB−99.98%
// previous artifact

Reuse the old install

Point the client at --previous-artifact. Shared ranges are copied from disk, not the network — no chunk cache required.

// signatures

Compact .cavssig

A deterministic old-version signature — 88 KiB for a 128 MiB build (~0.07%) — lets a new version be diffed without the old bytes.

// no-op

Skip unchanged work

An output that already matches costs 0 payload bytes. In directory mode, unchanged files — including player mods — are never rewritten.

// coalescing

Fewer, larger reads

Adjacent copy ranges merge: on the shifted build, 1,082 chunk ops → 18 contiguous reads, read amplification 1.0.

A corrupt old install demotes exactly the affected range to the network (CAVS-E-PREVIOUS-ARTIFACT-MISMATCH) and still ends byte-identical.
Scale — 1 GiB suite

FastCDC handles shifted
and reordered builds.

A deterministic 1 GiB benchmark suite (same seed ⇒ identical bytes on any machine) tests the update shapes that matter for chunked delivery. FastCDC 64 KiB + zstd 3.

Update shapeChangedPack timeUpdate egress
Small change~3% of blocks6.4 s51.3 MiB
Medium change~15%6.8 s216.6 MiB
Large change~50%6.8 s449.1 MiB
Head-shifted4 KiB inserted at head — every byte shifts7.3 s10.9 KiB
ReorderedSame blocks, 8 MiB groups swapped9.1 s20.8 MiB
A 4 KiB insertion at the beginning shifts every byte of a 1 GiB build. Fixed blocks would lose alignment — CAVS FastCDC resynchronizes and sends only 10.9 KiB.
v1.3.0 — small chunk profiles

16 KiB chunks cut real updates
up to 68% further.

Measured on the official Marble 1.6.0 → 1.6.1 Linux release builds (real PCK pair). The new fastcdc-16k / fastcdc-32k profiles use tight FastCDC normalization; the compact binary manifest keeps their metadata cost at a few KiB. Pack-time hashing and compression now run on all cores (3–7× faster, byte-identical output), which makes per-chunk zstd-19 practical for publishing.

ConfigUpdate wirevs previous defaultCold wire
fastcdc-64k + zstd-3 (previous default)1.50 MiB7.81 MiB
fastcdc-32k + zstd-30.80 MiB−47%8.01 MiB
fastcdc-16k + zstd-30.53 MiB−65%8.12 MiB
fastcdc-16k + zstd-190.48 MiB−68%7.50 MiB
The auto-profile sweep now prices manifests at their real binary-v2 weight and measures the small profiles on your actual bytes — and the dual bootstrap route keeps cold installs cheap regardless. Every reconstruction stays byte-identical.
Operational shape at rest

Store once, serve every jump.

Shared chunks are stored once. Packfile-backed stores collapse thousands of tiny chunk objects into a few immutable .cavspack files and coalesce nearby chunk reads into a few range reads — a deterministic object tree suitable for S3, R2 or static CDN.

// dedup at rest

Content-addressed dedup

Two versions of Marble: 13.80 MiB logical → 7.04 MiB on disk — roughly 49% less storage, shared chunks stored once. GC reclaims unreferenced chunks.

// many versions

10-version stream

Store + 3 policy-chosen hot-pair sidecars = 35.91 MiB serving any jump, vs 144.23 MiB for all-pairs one-hop coverage (45 patches) — 75% less. v1.1.0 also benchmarks the practical adjacent/ladder/base/hot-pair policies head-to-head.

// CDN export

Deterministic object tree

store export emits a stable object tree for static CDN hosting — same wire bytes as loose storage, far fewer objects to host.

GameChunk objects on diskPhysical reads (whole session)Read amplification
MechanicalFlower/Marble130 → 4130 → 2 (65×)1.000
GDQuest 3D third-person807 → 4805 → 7 (115×)1.000
godotengine/tps-demo5,775 → 65,775 → 34 (170×)1.000

Amplification 1.000 means coalescing read zero extra bytes: chunks are written in reconstruction order, so merged ranges are exactly contiguous.

Client cost

Constant memory, whatever the size.

The client reconstructs by streaming to disk — batch-decoded from the socket → disk cache → .part → SHA-256 → atomic rename — so RAM stays flat regardless of game size. Measured on the tps-demo update (569 MB game, release binaries).

7 MiB
Peak client RAM
update, streamed to disk
14.3 MiB
Peak RAM
cold bootstrap install
2.0 s
Update CPU
reconstruct + verify
3.5 s
Pack whole release
once per version

Resilience is measured too: an install killed with kill -9 at 57 MiB resumed with an HTTP Range request and downloaded only the remaining 166.5 MiB; a cache with 3 corrupted chunks (of 5,747) detected, quarantined and re-fetched exactly those 3, then returned to 0-byte re-fetches. A ~20-case corruption matrix and 5 libFuzzer targets leave zero unauthenticated-content survivors.

Honest negatives

Where CAVS saves nothing.

CAVS is not a codec and does not pretend to be. It is most valuable when redundancy crosses versions, releases, bundles or repeated assets.

Where redundancy does cross content (shared episode intros): 7–21% storage savings by codec; warm re-watch −100% egress. CAVS is strongest for game updates, engine packs, launchers, DLC, binary assets and version streams.
Reproducing

Run the suite yourself.

The synthetic large-build suite is fully reproducible from the source tree — a deterministic PRNG regenerates the exact same dataset on any machine and writes summary.md / summary.json.

$ cavs bench gen --out ds --size 1GiB
$ cavs bench suite --dataset ds --out results
# → results/summary.md, results/summary.json

The real-game harnesses and their raw result data live in the full development repository; the measured summaries above are what those harnesses produced. Full write-ups: docs/BENCHMARKS.md, docs/ROUTE_BENCHMARKS.md, docs/STEAMPIPE_COMPARISON.md, docs/results/v0.8.0/, docs/results/v0.9.0/.