Performance
Benchmarks
Section titled “Benchmarks”Measured on localhost dev server (wrangler dev) with a single Durable Object. Test data uses base64-encoded random data (worst case — incompressible).
Stress Test Results
Section titled “Stress Test Results”| Metric | 100 files (796K) | 1K files (7.8MB) | 5K files (39MB) |
|---|---|---|---|
| Push | 182ms | 585ms | 3.4s |
| Clone | 107ms | 686ms | 3.1s |
| Incremental push | 130ms | 372ms | 867ms |
| Stats API | 67ms | 186ms | 447ms |
| Files API | 65ms | 190ms | 435ms |
REST API Response Times
Section titled “REST API Response Times”| Endpoint | Latency |
|---|---|
| GET /api/repos (list) | 54ms |
| GET /api/repos/:o/:r (meta) | 44ms |
| GET /branches | 43ms |
| GET /log (50 commits) | 61ms |
| GET /files (root listing) | 42ms |
| GET /files/all (1K files) | 190ms |
| GET /stats (1K files) | 186ms |
| GET /contributors | 42ms |
Run benchmarks yourself:
./test/bench.sh # Quick benchmarksGITMODE_SERVER=http://localhost:8787 bash test/stress.sh quick # Stress test (100–10K files)Push Flow
Section titled “Push Flow”The push pipeline is optimized into distinct phases to minimize latency.
Optimizations
Section titled “Optimizations”R2 Chunk Storage
Section titled “R2 Chunk Storage”Problem: Each git object stored as an individual R2 key — 5K objects = 5K R2 operations on push and clone.
Solution: Objects are bundled into ~2MB R2 chunks during packfile unpack. A SQLite object_chunks table indexes each SHA to its chunk key, byte offset, and length. Reads use R2 range requests or fetch the full chunk and extract multiple objects at once.
Impact: 5K-file push reduced from 21s to 3.4s. Clone reads ~50 chunks instead of 5K individual objects.
Batch readObjects
Section titled “Batch readObjects”Problem: Clone and packfile building read objects one at a time — each R2 GET adds latency.
Solution: readObjects() groups requested SHAs by chunk key (via SQLite lookup), fetches each chunk once with 10-concurrent R2 GETs, then extracts and decompresses all objects from the chunk data.
Impact: 5K-file clone reduced from 4.7s to 3.1s.
Streaming Clone Response
Section titled “Streaming Clone Response”Problem: Upload-pack buffered the entire packfile, then wrapped it in sideband packets, then copied everything into a response buffer — 3x the packfile size in memory.
Solution: Use a ReadableStream to emit sideband-wrapped chunks directly from the packfile data. Peak memory stays at ~1x packfile size.
Impact: Enables cloning repos up to ~40MB of data within DO memory limits.
Async Worktree Materialization
Section titled “Async Worktree Materialization”Problem: Worktree writes (5K R2 PUTs for a 5K-file push) blocked the push response and subsequent requests.
Solution: For pushes with >500 objects, worktree materialization is deferred via ctx.waitUntil(). The checkout loop yields between 500-object batches with setTimeout(0) so the DO can serve concurrent requests (e.g., an immediate clone after push).
Impact: Push response returns immediately for large repos. Clone can interleave with background worktree writes.
Incremental Worktree Updates
Section titled “Incremental Worktree Updates”Problem: Every push rewrote all worktree files, even if only one file changed.
Solution: materializeWorktree() diffs old tree vs new tree, only writes changed/added files, deletes removed files.
Impact: Incremental push for a single file change: 130ms (100 files) to 867ms (5K files).
Zero-Copy WASM Memory
Section titled “Zero-Copy WASM Memory”Problem: Every WASM call copied data in (.set()) and out (.slice()). Chained operations like hash→deflate did 2 full heap resets and 2 unnecessary intermediate copies per object.
Solution: Three techniques eliminate copies:
viewBytes()— returns asubarrayview into WASM memory instead of.slice(). Used in packfile building where the deflate result is immediately consumed byentry.set().hashAndDeflate()— fused pipeline: content written to WASM once, SHA-1 computed, header+content assembled in WASM memory, then deflated. One reset instead of two.heapSave()/heapRestore()— checkpoint the bump allocator so multiple results can coexist without a full reset.
Impact: For a 5K-object push, prepareObject alone avoids ~500MB of memcpy (2 copies eliminated per object).
SQLite-First Object Lookups
Section titled “SQLite-First Object Lookups”Problem: hasObject() made an R2 HEAD request per SHA during fetch negotiation.
Solution: Check the object_chunks SQLite index first — pure SQLite query, no R2 round trip. Only falls back to R2 HEAD for objects not in the chunk index.
Impact: Eliminates R2 round trips during have negotiation in fetch/clone.
Cost Analysis
Section titled “Cost Analysis”R2 pricing (as of 2026):
- Class A (PUT/DELETE): $4.50 per million operations
- Class B (GET/LIST): $0.36 per million operations
- Storage: $0.015 per GB/month
Per-push costs for a 1K-file repo:
- Object writes: ~5 chunk PUTs = $0.0000225
- Worktree writes: ~5 PUTs (incremental) = $0.0000225
- Object reads: 0 (optimistic cache) = $0
For 1,000 pushes/month to a 1K-file repo: ~$0.045 total R2 costs (chunk storage reduces PUT count by ~200x).