1---
2title: Scaling
3description: Reads scale like a CDN and writes scale like git. The shape, the per-repository limits, the measured numbers, and a worked example of pushing something large and swapping the hasher.
4---
5
6The examples use `Authentication` from
7[Getting Started](/git/getting-started): the application's request middleware.
8
9Every repository is its own Durable Object, so the service's capacity
10is the sum of its repositories. Within one repository the two
11directions scale differently, and the difference is git's, not the
12host's.
13
14**Reads scale out.** A full clone streams a precomputed bundle from
15the blob store through the Worker. The repository's Durable Object
16plans the clone and never touches a pack byte, so concurrent clones
17scale with Workers and object storage. Incremental fetches are small
18by construction and run against a commit graph in SQLite.
19
20**Writes serialize on the ref.** A push is a compare-and-swap on a
21ref, and git's own semantics say two pushes to one branch cannot both
22win. One Durable Object per repository is that serialization point.
23Receiving, hashing, and staging the pack happen in the Worker and fan
24out to the [hasher](/git/blocks/hasher).
25
26| Dimension | Per repository | Bound by |
27| --- | --- | --- |
28| Clone bandwidth | edge line rate | bundle bytes live in the blob store |
29| Clone rate | thousands per second | the advertisement is cached, the Durable Object is called once per push |
30| Incremental fetch | milliseconds for recent tips | the commit graph in SQLite |
31| Push rate | 5–20 per second | a ref is a serialization point in git's own semantics |
32| Repository size | unbounded | packs live in the blob store |
33| Push size | unbounded | the body streams to the blob store as it arrives |
34
35## Where the limits are
36
37**Push rate to one branch** is inherent. Across branches and across
38repositories there is no shared limit.
39
40**The registry** is one object handling creates, deletes, and name
41lookups for the whole service. Lookups are cached at the Worker for a
42minute, and [D1](/git/blocks/registry) replicates them.
43
44**First-byte latency** for a repository's Durable Object is one
45datacenter's distance. Cached advertisements hide most of it, and
46clone bytes come from the blob store rather than the object.
47
48**Very large histories.** Computing which objects a fetch needs walks
49the commit graph. For repositories with tens of millions of objects
50that walk wants reachability bitmaps, which the service does not have
51yet.
52
53## A large push, and the hasher
54
55Push a repository with a 40 MiB history to the host from
56[Getting Started](/git/getting-started) and
57read the timing the repository reports:
58
59```sh
60curl -s -u "x:$GIT_SECRET" "$HOST/api/v1/repos/acme/big" | jq .lastPush
61```
62
63Ingest dominates. Every object was inflated and hashed on the one
64thread that received the body. Swap the hasher and deploy:
65
66```diff lang="typescript"
67+import * as GitHasher from "alchemy/Git/Hasher";
68
69const GitLive = Git.ApiLive.pipe(
70 Layer.provide(Git.ApiHandlersLive),
71 Layer.provide(Authentication.layer),
72 Layer.provide(Git.ReposDurableObject),
73 Layer.provide(Git.RegistryDurableObject),
74- Layer.provide(Git.HasherInline),
75+ Layer.provide(GitHasher.HasherWorkerLoader()),
76 Layer.provide(Git.BlobStoreR2(GitObjects)),
77);
78```
79
80Four dynamically loaded Workers now hash four 4 MiB chunks at once,
81each in its own isolate. Push again, read `lastPush` again. Nothing
82else about the host changed.
83
84One client, the same edge, a fresh repository on each host. The
85delta-heavy pack is 40 MiB and 15.6k objects, the whole-blob pack is
8644 MiB:
87
88| | 40 MiB deltas | 44 MiB blobs | incremental |
89| --- | --- | --- | --- |
90| GitHub | 3.3 s | 4.6 s | 1.2 s |
91| `HasherInline` | 9.9–13.5 s | 8.8 s | 0.2–0.4 s |
92| `HasherWorkerLoader` | 5.9 s best, 6.5 s median | 4.8 s best, 6.4 s median | 0.2–0.4 s |
93| `HasherLambda` | 6.8–7.9 s | 6.3–6.7 s | 0.3–0.8 s |
94
95Incremental pushes are the same on every hasher, so the choice only
96matters for pushes of tens of megabytes. `HasherLambda` runs every
97chunk at once on a 3 GB function and needs an AWS account in the
98stack. [Hasher](/git/blocks/hasher) has the push path and each
99implementation. [Bytes in S3, hashing on Lambda](/git/recipes/cloudflare-aws)
100is the two-cloud stack.
101
102## Clones
103
104The same 36 MiB, 15.6k-object repository, one client, the same edge:
105
106| Path | Throughput |
107| --- | --- |
108| a real `git clone` from a bundle | 32.7 MiB/s typical, 80 MiB/s best (0.69 s end to end) |
109| raw bundle, four concurrent streams | 93 MiB/s aggregate |
110| dynamic fetch, time to first byte | 0.43–0.56 s |
111| dynamic fetch, transfer | 7–10 MiB/s |
112
113The bundle path is the client's line rate. Compaction is what keeps it
114that way: on a real repository of 13,699 objects, cloning back took
11519.5 s before compaction and 3.4 s after.
116[Repository](/git/blocks/repositories) has the clone path.
117
118The largest real push measured is the alchemy monorepo: 44,051 objects
119in a 67 MiB thin pack, cloned back byte-identical under
120`git fsck --strict`.