Skip to content

Performance

Measured on localhost dev server (wrangler dev) with a single Durable Object. Test data uses base64-encoded random data (worst case — incompressible).

Metric100 files (796K)1K files (7.8MB)5K files (39MB)
Push182ms585ms3.4s
Clone107ms686ms3.1s
Incremental push130ms372ms867ms
Stats API67ms186ms447ms
Files API65ms190ms435ms
EndpointLatency
GET /api/repos (list)54ms
GET /api/repos/:o/:r (meta)44ms
GET /branches43ms
GET /log (50 commits)61ms
GET /files (root listing)42ms
GET /files/all (1K files)190ms
GET /stats (1K files)186ms
GET /contributors42ms

Run benchmarks yourself:

Terminal window
./test/bench.sh # Quick benchmarks
GITMODE_SERVER=http://localhost:8787 bash test/stress.sh quick # Stress test (100–10K files)

Push Flow

The push pipeline is optimized into distinct phases to minimize latency.

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.

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.

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.

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.

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).

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 a subarray view into WASM memory instead of .slice(). Used in packfile building where the deflate result is immediately consumed by entry.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).

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.

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).