How the Content-Addressed Blob Cache Works in @cloudflare/dofs: Implementation and Performance Deep Dive
The content-addressed blob cache in @cloudflare/dofs is a per-database, in-process LRU cache that stores raw file chunk bytes indexed by SHA-256 hash, eliminating redundant SQLite queries during sequential reads.
The @cloudflare/dofs library implements a distributed object file system backed by SQLite, and its blob cache serves as a critical optimization layer between FUSE read operations and persistent storage. Understanding this cache mechanism is essential for developers building performant file system integrations with Cloudflare's edge infrastructure.
Why the Blob Cache Exists
FUSE read operations request data in 128 KiB windows, while @cloudflare/dofs persists data in 512 KiB chunks. This mismatch creates a fundamental I/O inefficiency: without intermediate caching, a sequential read through a large file would query the same SQLite blob multiple times. The problem compounds with deduplicated content—a zero-filled 4 MiB file might reference a single blob hundreds of times, triggering identical database lookups for every read window.
The cache addresses this by keeping hot blob bytes in memory, reducing SQLite round-trips from one per read window to a single lookup per distinct hash.
Core Architecture: Content-Addressed Storage
SHA-256-Based Keying
Blobs are indexed by their 32-byte SHA-256 content hash. In packages/dofs/src/fs/blobCache.ts, this hash transforms into a Latin-1 string (hashKey) for efficient Map lookup:
- Binary hash → string conversion avoids hex-encoding overhead
- Content addressing guarantees immutable, collision-resistant cache entries
- Identical content automatically shares cache entries regardless of file location
// Conceptual representation from blobCache.ts
function hashToKey(hash: Uint8Array): string {
// Decodes binary hash as Latin-1 for cheap string keys
return new TextDecoder('latin1').decode(hash);
}
Per-Database Isolation
The cache uses a WeakMap<Database, Map<string, Uint8Array>> structure:
- Each SQLite database receives an isolated cache instance
- WeakMap allows garbage collection when database connections close
- Prevents cross-test contamination in test suites running multiple databases
LRU Eviction and Memory Bounds
The cache enforces strict memory discipline through a bounded LRU policy:
| Parameter | Value | Purpose |
|---|---|---|
CHUNK_CACHE_MAX_ENTRIES |
16 entries | Caps memory per database |
| Eviction strategy | Insertion-order LRU | O(1) deletion of oldest entry |
| Promotion on access | Delete + re-insert | Moves entries to "most recent" position |
When getBlobBytes() stores a new blob exceeding the limit, it evicts the first key in insertion order—the oldest, least-recently-used entry. Accessing an existing entry promotes it by removing and re-inserting, maintaining accurate recency tracking without complex data structures.
Cache Lifecycle: Lookup, Miss, and Clear
1. Cache Lookup
The getBlobBytes(db, hash) function in packages/dofs/src/fs/blobCache.ts performs O(1) hash-key lookup:
import { getBlobBytes } from "@cloudflare/dofs/src/fs/blobCache.js";
// Returns cached Uint8Array or undefined on miss
const bytes = getBlobBytes(db, contentHash);
// Caller must treat returned array as read-only
Cache hits return the same Uint8Array instance—callers must not mutate this buffer.
2. Cache Miss Handling
On miss, the function:
- Queries
vfs_blob_bytestable for the blob - Stores result in the per-database Map
- Evicts excess entries if over
CHUNK_CACHE_MAX_ENTRIES
// Internal flow (simplified)
const cached = cache.get(db)?.get(hashKey);
if (cached) return cached; // Hit path
// Miss path: fetch from SQLite, then cache
const bytes = db.query("SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", [hash]);
cache.get(db)?.set(hashKey, bytes);
evictIfNeeded(cache.get(db)!);
3. Explicit Cache Clearing
clearBlobCache(db) deletes the entire cache for a database. Test suites rely on this for deterministic isolation between runs:
import { clearBlobCache } from "@cloudflare/dofs/src/fs/blobCache.js";
// Guarantee clean state before performance-sensitive operations
clearBlobCache(db);
// ... run benchmark or test ...
System Integration Points
Read Path Integration
File read operations throughout @cloudflare/dofs leverage getBlobBytes():
readFile()inpackages/dofs/src/fs/readFile.ts— assembles full file contentsreadRangeSync()— random access readsProviderimplementation inpackages/dofs/src/provider.ts— higher-level API surface
Each call consults the cache, ensuring sequential reads across the same 512 KiB chunk hit memory after the first SQLite fetch.
Write Path Consistency
The write path (upsertChunkBlob in packages/dofs/src/fs/writeFile.ts) maintains cache validity through an important invariant: existing (hash, bytes) pairs are never overwritten. Content addressing guarantees that if a hash exists, its bytes are immutable.
Cache invalidation occurs only during repair operations (stageBlob), which explicitly clear the cache to prevent serving stale data:
// From writeFile.ts implementation pattern
// Normal write: new hashes, no cache impact
await upsertChunkBlob(db, hash, bytes);
// Repair operation: must clear cache
stageBlob(db, hash, newBytes);
clearBlobCache(db); // Prevent stale hits
Performance Impact
The blob cache delivers measurable latency reductions for access patterns common in edge file systems:
| Scenario | Without Cache | With Cache |
|---|---|---|
| Sequential 4 MiB zero-filled file | 32 SQLite queries | 1 SQLite query + 31 memory hits |
| Random access within same chunk | 1 query per access | 1 query total |
| Large deduplicated files | Linear query growth | Sublinear based on unique chunks |
By bounding cache size to 16 entries (approximately 8 MiB worst-case), the implementation balances hit rate against memory pressure for multi-tenant edge environments.
Working Example: Cache Behavior in Practice
import { withDB } from "@cloudflare/dofs/src/fs/with-db.js";
import { readFile, readRangeSync } from "@cloudflare/dofs/src/fs/readFile.js";
import { writeFileSync } from "@cloudflare/dofs/src/fs/writeFile.js";
import { clearBlobCache } from "@cloudflare/dofs/src/fs/blobCache.js";
await withDB(async (db) => {
// Create 4 MiB file deduplicated to single 512 KiB blob (all zeros)
const payload = new Uint8Array(4 * 1024 * 1024).fill(0);
await writeFileSync(db, "/big.bin", payload, {}, () => Date.now());
// Reset cache for clean observation
clearBlobCache(db);
// Sequential 128 KiB reads: first fetches from SQLite, rest hit cache
const window = 128 * 1024;
for (let offset = 0; offset < payload.byteLength; offset += window) {
const chunk = readRangeSync(db, "/big.bin", offset, window);
// Iterations 2-32 return cached Uint8Array without database query
}
// Full file read also benefits from populated cache
const contents = await readFile(db, "/big.bin");
});
Key Implementation Files
Understanding the cache requires examining these source files in the cloudflare/computer repository:
| File | Role |
|---|---|
packages/dofs/src/fs/blobCache.ts |
LRU cache implementation, getBlobBytes(), clearBlobCache() |
packages/dofs/src/fs/blobCache.test.ts |
Validation of reuse, eviction, and LRU ordering |
packages/dofs/src/fs/readFile.ts |
Read paths demonstrating cache integration |
packages/dofs/src/provider.ts |
Higher-level API using cached blob access |
packages/dofs/src/fs/writeFile.ts |
Write path and cache invalidation during repair |
Summary
- The content-addressed blob cache eliminates redundant SQLite queries by storing 512 KiB file chunks in memory, indexed by SHA-256 hash.
- Per-database isolation via
WeakMapprevents cross-contamination, while a 16-entry LRU bound controls memory consumption. - Cache hits return read-only
Uint8Arrayinstances—mutating these corrupts cache state for subsequent callers. - Write operations preserve cache validity through content-addressing immutability; only explicit repair operations require cache clearing.
- Integration spans
readFile.ts,provider.ts, and internal read paths, making the cache transparent to API consumers.
Frequently Asked Questions
How does the content-addressed blob cache handle hash collisions?
SHA-256 provides 256-bit collision resistance, making accidental collisions computationally infeasible. The cache treats hash equality as content equality—if two distinct inputs produced the same hash, they would incorrectly share cache entries. In practice, this matches @cloudflare/dofs's overall content-addressed storage design where hash collisions would corrupt data regardless of caching.
Why does the cache use Latin-1 string encoding for hash keys instead of hex or base64?
Latin-1 (ISO-8859-1) provides a 1:1 byte-to-character mapping with zero encoding overhead. A 32-byte hash becomes a 32-character string, whereas hex encoding would double to 64 characters and base64 to approximately 44. This reduces memory per key and Map lookup overhead while remaining compatible with JavaScript string internals.
What happens when CHUNK_CACHE_MAX_ENTRIES is exceeded?
The cache evicts the least-recently-used entry by deleting the first key in the Map's insertion order. JavaScript Maps maintain insertion order, making this O(1) without auxiliary data structures. The newly inserted entry becomes the "most recent," and subsequent accesses promote entries by delete/re-insert operations.
Can the blob cache be disabled or tuned for different workloads?
The current implementation in @cloudflare/dofs hardcodes CHUNK_CACHE_MAX_ENTRIES at 16. Workload-specific tuning would require modifying packages/dofs/src/fs/blobCache.ts. The cache design assumes edge deployment constraints—small working sets with high temporal locality—rather than large-file streaming scenarios that might benefit from larger bounds.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →