| 1 | # Cloudflare Artifacts: due diligence for g1t at launch scale |
| 2 | |
| 3 | Status: research document, 2026-10-06. Read-only review: no code, config or production data was changed. |
| 4 | Scope: everything g1t stores in Cloudflare Artifacts (open beta since 2026-10-01; billing from 2026-10-14), |
| 5 | measured against what Cloudflare documents, and what we must build so that a few thousand workspaces |
| 6 | can run on it. |
| 7 | |
| 8 | ## Summary |
| 9 | |
| 10 | Artifacts fits what g1t does: one Durable Object per repository, smart HTTP for clones and pushes, a |
| 11 | read-only binding for commits, trees and blobs, forks and short-lived tokens. Nothing we rely on is |
| 12 | missing outright. Five things are not yet safe at a few thousand workspaces. |
| 13 | |
| 14 | 1. **We do not know what an "operation" is.** Pricing says "repo operations, such as `create`, `push`, |
| 15 | `pull`, and `clone`". Metrics list only `create`, `fork`, `push`, `pull`, `delete`. If binding reads |
| 16 | (`get`, `info`, `createToken`, `log`, `readTree`, `readBlob`, `readCommit`, `readFile`) and every git |
| 17 | HTTP request also count, our bill at launch scale is roughly **15× larger** (about $31k a month instead |
| 18 | of about $1.8k). We must settle this before 2026-10-14. |
| 19 | 2. **Pull request forks are never deleted.** Every pull request is an Artifacts fork (`pulls--<id>`). They |
| 20 | are purged only with their parent repository. Cloudflare does not say whether a fork shares objects |
| 21 | with its source. If forks copy objects, an agent-heavy workload reaches the **1 TB account storage |
| 22 | limit in days**, and every push then fails. If they share, storage still grows without bound. |
| 23 | 3. **One namespace carries everything.** All repositories and forks live in the `g1t` namespace. The |
| 24 | control-plane limit is **2,000 requests per 10 seconds per namespace** (200 per second). If binding |
| 25 | calls count against it, page views, token mints and mergeability checks together exceed it at launch |
| 26 | peaks. |
| 27 | 4. **Hard limits are not enforced in front of Artifacts.** 1 GB per repository, 32 MB per file, and a |
| 28 | 128 MB Worker isolate that buffers each push body twice. Large pushes and imports fail late, without a |
| 29 | message git can show. |
| 30 | 5. **No backup, no exit drill.** Cloudflare replicates data, but there is no SLA, no documented export |
| 31 | besides git itself, and the self-host git store is not a production fallback yet. |
| 32 | |
| 33 | None of these blocks an invite-only launch. Items 1 and 2 must be answered before billing starts on |
| 34 | 2026-10-14, and the fork cleanup must ship before agent pull requests reach thousands a day. |
| 35 | |
| 36 | ## 1. What Cloudflare documents |
| 37 | |
| 38 | All quotes are from developers.cloudflare.com, retrieved 2026-10-06. |
| 39 | |
| 40 | ### Limits ([Artifacts limits](https://developers.cloudflare.com/artifacts/platform/limits/), updated 2026-10-01) |
| 41 | |
| 42 | | Limit | Value | |
| 43 | | --- | --- | |
| 44 | | Control-plane request rate | 2,000 requests per 10 seconds **per namespace** | |
| 45 | | Git request rate | 2,000 requests per 10 seconds **per repository** | |
| 46 | | Storage per repository | **1 GB** | |
| 47 | | Largest file or blob | **32 MB** | |
| 48 | | Storage per account | **1 TB** (can be raised on request) | |
| 49 | | Repositories, namespaces | Unlimited | |
| 50 | | Names | 2 to 63 characters; letters, digits, `.`, `_`, `-`; start with a letter or digit | |
| 51 | |
| 52 | ### Pricing ([Artifacts pricing](https://developers.cloudflare.com/artifacts/platform/pricing/)) |
| 53 | |
| 54 | - Workers Paid only. "Cloudflare will begin billing for Artifacts operations and storage on October 14, 2026." |
| 55 | - Operations: first 10,000 a month, then **$0.15 per 1,000**. Defined only as "the number of repo |
| 56 | operations, such as `create`, `push`, `pull`, and `clone`." |
| 57 | - Storage: first 1 GB, then **$0.50 per GB-month**, "calculated by averaging peak storage per day over a |
| 58 | 30-day billing period". "Replicas do not add storage charges." "Repos remain stored until you |
| 59 | explicitly delete them." |
| 60 | |
| 61 | ### Architecture ([announcement blog](https://blog.cloudflare.com/artifacts-git-for-agents-beta/), 2026-04-16, Matt Carey and Matt Silverlock) |
| 62 | |
| 63 | - Each repository is a Durable Object; the git server is Zig compiled to WebAssembly (about 100 KB). |
| 64 | - "Files are stored in the underlying Durable Object's SQLite database." "Durable Object storage has a |
| 65 | 2MB max row size, so large Git objects are chunked and stored across multiple rows." |
| 66 | - "DOs have ~128MB memory limits." Fetch and push stream (`ReadableStream<Uint8Array>`). |
| 67 | - "Artifacts also uses R2 (for snapshots) and KV (for tracking auth tokens)." |
| 68 | - Deltas are stored as received; "if the requesting client already has the base object, Zig emits the |
| 69 | delta instead." |
| 70 | - Durability ([How Artifacts works](https://developers.cloudflare.com/artifacts/concepts/how-artifacts-works/)): |
| 71 | "Cloudflare replicates repo data synchronously across multiple data centers and copies it |
| 72 | asynchronously to object storage and snapshots." |
| 73 | - A repository is "a single logical instance that Cloudflare can route to from any region", like a |
| 74 | Durable Object. Durable Objects have a soft limit of 1,000 requests per second each |
| 75 | ([DO limits](https://developers.cloudflare.com/durable-objects/platform/limits/)). |
| 76 | |
| 77 | ### Git protocol ([Git protocol](https://developers.cloudflare.com/artifacts/api/git-protocol/)) |
| 78 | |
| 79 | | Feature | Documented support | |
| 80 | | --- | --- | |
| 81 | | Clone and fetch | Protocol v1 and v2 (`ls-refs`, `fetch`); v1 shallow and deepen | |
| 82 | | Push | v1 receive-pack only; v2 receive-pack not supported | |
| 83 | | `filter` (partial clone), `include-tag` | "not supported" for v1 | |
| 84 | | Tokens | `art_v1_<40 hex>?expires=<unix seconds>`; scopes `read`, `write`; TTL 60 s to 1 year, default 24 h | |
| 85 | |
| 86 | **Measured differently:** a protocol v2 `git clone --filter=blob:none` of `flagon-io/g1t` through g1t |
| 87 | returned a real partial clone (2,789 objects, 903 KB) instead of the full 6,187 objects and 5.5 MB. So |
| 88 | v2 filtering works today, despite the table. Ask Cloudflare whether this is supported or accidental. |
| 89 | |
| 90 | ### Binding API ([Workers binding](https://developers.cloudflare.com/artifacts/api/workers-binding/); generated types in `services/*/worker-configuration.d.ts`) |
| 91 | |
| 92 | - Namespace: `create`, `get`, `list`, `import`, `delete`. `get()` is a lookup that throws `NOT_FOUND` or |
| 93 | `*_IN_PROGRESS`, so it costs a round trip. |
| 94 | - Repository handle: `info()` ("Each call performs a fresh lookup"), `createToken`, `listTokens`, |
| 95 | `revokeToken`, `log` (first-parent only, at most 1,000), `readCommit`, `readTree` (one level), |
| 96 | `readBlob`, `readFile`, `fork` (`defaultBranchOnly` defaults to true). |
| 97 | - Not available: listing refs, updating refs, writing objects, repository size, gc or repack, hooks. |
| 98 | g1t works around the first three over smart HTTP (`refs.rs`, `land.rs`, `catch_up.rs`). |
| 99 | - Errors: `MEMORY_LIMIT` (10402) "if the object cannot be buffered safely", `INTERNAL_ERROR` (10400), |
| 100 | `UPSTREAM_UNAVAILABLE`, `IMPORT_IN_PROGRESS`, `FORK_IN_PROGRESS`, `CREATE_IN_PROGRESS` |
| 101 | ([Errors](https://developers.cloudflare.com/artifacts/api/errors/)). |
| 102 | - Doc drift: the [isomorphic-git example](https://developers.cloudflare.com/artifacts/examples/isomorphic-git/) |
| 103 | still says the binding "cannot read or write files inside them". It can read; it cannot write. |
| 104 | |
| 105 | ### Observability, events, data location |
| 106 | |
| 107 | - Metrics: GraphQL dataset `artifactsEventsAdaptiveGroups`, 31 days, with `eventType` in `create`, |
| 108 | `fork`, `push`, `pull`, `delete`, and errors `storageLimitReached`, `serverError`, `clientError`, |
| 109 | `rateLimited` ([Metrics](https://developers.cloudflare.com/artifacts/observability/metrics/)). |
| 110 | **This is the fastest way to learn what Cloudflare counts.** |
| 111 | - Events: account level `repo.created|deleted|forked|imported`; per-repository `pushed`, `cloned`, |
| 112 | `fetched`, `token.created|revoked`, which need one subscription per repository |
| 113 | ([Event subscriptions](https://developers.cloudflare.com/artifacts/guides/event-subscriptions/)). |
| 114 | g1t reports pushes itself for that reason (`record_push`). |
| 115 | - Jurisdictions `eu` and `us`, chosen when a namespace is created and never changeable |
| 116 | ([Data localization](https://developers.cloudflare.com/artifacts/guides/data-localization/)). A binding |
| 117 | names one namespace, so EU residency needs a second binding. |
| 118 | - Best practices: "Do not keep every repo in one default namespace once usage grows" and "When one |
| 119 | namespace becomes hot, shard new repos into additional namespaces" |
| 120 | ([Best practices](https://developers.cloudflare.com/artifacts/concepts/best-practices/)). |
| 121 | - Beta: the docs state no SLA, no support tier, and no backup or export feature beyond git itself. |
| 122 | |
| 123 | ### Platform limits that bound us |
| 124 | |
| 125 | | Limit | Value | Source | |
| 126 | | --- | --- | --- | |
| 127 | | Worker memory | 128 MB per isolate, shared by concurrent requests | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) | |
| 128 | | Request body | 100 MB on Free and Pro zones, 200 MB Business, up to 5 GB Enterprise | same | |
| 129 | | Response body | No limit (CDN cache 512 MB) | same | |
| 130 | | CPU | 30 s default, up to 5 min | same | |
| 131 | | Subrequests | 10,000 per invocation (Paid); 6 connections waiting for headers at once | same | |
| 132 | | Cache API | Per data center only; calls share the subrequest quota | [Cache](https://developers.cloudflare.com/workers/runtime-apis/cache/) | |
| 133 | | KV | 1 write per second per key; 25 MiB values; 60 s minimum TTL | [KV limits](https://developers.cloudflare.com/kv/platform/limits/) | |
| 134 | | D1 | 10 GB per database; 30 s per query; one writer | [D1 limits](https://developers.cloudflare.com/d1/platform/limits/) | |
| 135 | | Cloudflare REST API | 1,200 requests per 5 minutes per user | [API rate limits](https://developers.cloudflare.com/fundamentals/api/reference/limits/) | |
| 136 | | Containers | Up to 4 vCPU, 12 GiB, 20 GB disk (ephemeral) | [Containers limits](https://developers.cloudflare.com/containers/platform-details/limits/) | |
| 137 | | Smart Placement | Applies to fetch handlers, not RPC entrypoints | [Placement](https://developers.cloudflare.com/workers/configuration/placement/) | |
| 138 | |
| 139 | The REST API's 1,200 requests per 5 minutes means the hot path must use the binding, never the REST API. |
| 140 | |
| 141 | ## 2. How g1t uses Artifacts |
| 142 | |
| 143 | All Artifacts traffic goes through `services/repos` (`g1t-repos`), binding `ARTIFACTS`, namespace `g1t`, |
| 144 | placement off. Store keys are `<workspace>--<repo>`; pull request forks are `pulls--<pull id>`. |
| 145 | |
| 146 | ### Calls per user action |
| 147 | |
| 148 | "Binding" counts calls on `env.ARTIFACTS`; "git" counts HTTP requests to the repository's remote. |
| 149 | |
| 150 | | Action | Path in code | Binding calls | Git requests to Artifacts | |
| 151 | | --- | --- | --- | --- | |
| 152 | | Credential (mint) | `store.rs` `mint_access` | 3: `get`, then `info` and `createToken` at once | 0 | |
| 153 | | Clone, protocol v2 | `lib.rs` `answer_git` → `git_http::forward` | mint if none kept (3) | `info/refs` and `ls-refs` (0 on a `refs_cache` hit), `fetch` 1 | |
| 154 | | Clone, protocol v0 | same | mint if none kept | `info/refs` (0 on hit), `upload-pack` 1 | |
| 155 | | Fetch, nothing new | same | as above | `ls-refs` (0 on hit), `fetch` 1 | |
| 156 | | Push | same, then `record_push` | mint; then `get` + `log(branch, 1)` per pushed branch | `info/refs` 1, `receive-pack` 1 | |
| 157 | | Tree page | `Repos::tree` | `get`, `log(ref, 1)`, `readTree` per path segment (Cache API), `readBlob` README (cached) | 0 | |
| 158 | | File page | `Repos::blob` | `get`, `readFile` (not cached) | 0 | |
| 159 | | Branches list | `GitRepo::branches` (`refs.rs`) | mint (3) | `info/refs` 1 (does not use `refs_cache`) | |
| 160 | | Blame | `blame.rs` | `log(ref, 400)`, then trees and blobs per commit | 0 (one view took 6.6 s wall in the tail) | |
| 161 | | Open a pull request | `fork_for_pull` | `get`, `fork` | 0 | |
| 162 | | Mergeability (each time the target branch moves, for up to 100 open pull requests) | `divergence` | `get` ×2, `log(…, 1000)` ×2, `readCommit` per commit off the first-parent chain, `readTree` per changed directory | 0 | |
| 163 | | Catch-up (no conflicts) | `catch_up.rs` | `get` ×2, `log` ×2, trees | `upload-pack` 1, `receive-pack` 1 | |
| 164 | | Land a pull request | `land.rs` | 2 mints (6) | `upload-pack` 1 from the fork, `receive-pack` 1 to the target | |
| 165 | | Commit one file | `commit_file.rs` | mint, `log` | `info/refs` 1, `receive-pack` 1 | |
| 166 | | Mirror sync or import | `mirror.rs`, `import.rs` | mint | `info/refs` 1–2, `upload-pack` and `receive-pack` 1 each; packs capped at 40 MB | |
| 167 | | Search indexing | `listing.rs` | trees by level, blobs in groups | 0 | |
| 168 | | Push protection | `secret_scan.rs` | trees and blobs for bases, up to 24 MB scanned | 0 | |
| 169 | | Delete (purge) | `lifecycle.rs` | `delete` per key, including forks | 0 | |
| 170 | |
| 171 | Every sandbox job clones in full (`crates/runner/src/{main,checks,review,plan,queue,reply,update,deploy,mergecheck}.rs`: |
| 172 | `git clone --quiet`, no depth, no filter), and most then fetch an upstream branch. Only Actions' |
| 173 | checkout fetches with `--depth=1` (`crates/runner/src/actions/uses.rs`). |
| 174 | |
| 175 | ### What already saves calls |
| 176 | |
| 177 | | Mitigation | Where | What it saves | |
| 178 | | --- | --- | --- | |
| 179 | | Credential cache | `store.rs` `Credentials` (per isolate) and `shared.rs` (KV, sealed), TTL 300 s, reused 180 s | 3 binding calls and about 800 ms per git request on a hit | |
| 180 | | Ref listing cache | `refs_cache.rs`, keyed by `refs_version`, 60 s TTL, colo Cache API then KV | the `info/refs` and `ls-refs` round trip (330–400 ms) on a hit; measured hits answer in 3–6 ms | |
| 181 | | Object cache | `store.rs` `cached`/`keep`, Cache API, immutable | `readTree`, `readBlob` (≤ 1 MB), and `log` by commit hash | |
| 182 | | Recent row cache | `registry.by_path_recent` | D1, not Artifacts | |
| 183 | | Soft delete | `lifecycle.rs` | an accidental `delete` cannot lose data for `RESTORE_DAYS` | |
| 184 | |
| 185 | Gaps in those caches: the Cache API is per data center, so each colo pays its own misses; `log` by branch |
| 186 | name, `readFile`, `branches()` and `readCommit` are never cached; the credential reuse window is 180 s, |
| 187 | so a busy repository mints about 20 times an hour per scope. |
| 188 | |
| 189 | ## 3. Measured in production |
| 190 | |
| 191 | Read-only, from a client in Denver (`CF-Ray …-DEN`), 2026-10-06 07:40 UTC, git 2.45 user agent. |
| 192 | `flagon-io/hello`: 239 objects, 44 KB pack. `flagon-io/g1t`: 6,187 objects, 5.5 MB pack. |
| 193 | |
| 194 | | Measurement | hello | g1t | |
| 195 | | --- | --- | --- | |
| 196 | | `info/refs`, cold: credential minted | n/a | 1,306–1,425 ms total: `mint` 807–856 ms, `store` 334–399 ms | |
| 197 | | `info/refs`, `refs_cache` hit | 3–6 ms server, 80–150 ms client | 3–6 ms server | |
| 198 | | `ls-refs` / v2 advertisement miss with kept credential | 436 ms (`store` 349) | 458 ms (`store` 332) | |
| 199 | | Full upload-pack (replayed v0 want, kept credential) | 674–1,104 ms, `store` 607–997 | 1,960–2,223 ms to first byte, `store` 1,855–2,156; 3.2 s total | |
| 200 | | `git clone --bare`, first / repeats | 5.1 s / 2.0–2.4 s | 5.4 s / 4.2 s (4.8–5.8 s during the tail) | |
| 201 | | `git fetch`, nothing new | 0.99 s first, then 0.39–0.42 s | 0.95 s first, then 0.35–0.41 s | |
| 202 | | `clone --depth=1` | 1.6 s | 3.8 s | |
| 203 | | `clone --filter=blob:none` | 1.4 s | 3.4 s, 903 KB | |
| 204 | | `kept` step on every POST | 63–98 ms | same | |
| 205 | |
| 206 | Observations: |
| 207 | |
| 208 | - Artifacts builds the whole pack before the first byte: about 2 s for 5.5 MB. Every full clone pays |
| 209 | this, every time, because nothing caches packs. |
| 210 | - Minting a credential costs about 0.8 s, almost all in `get` then `info` and `createToken`. |
| 211 | - The 63–98 ms `kept` step on every POST is not Artifacts. It is `git_limits` doing a D1 upsert into |
| 212 | `git_operations` before the request is forwarded. |
| 213 | - `wrangler tail g1t-repos` for 75 s: 87 events, all `ok`, no exceptions or error logs. The slowest were a |
| 214 | blame RPC (6.6 s wall, 178 ms CPU) and an `info/refs` miss with a mint (2.6 s). |
| 215 | |
| 216 | ## 4. Where we and the docs disagree |
| 217 | |
| 218 | | # | Topic | Documented | What g1t does or assumes | Risk | Fix | |
| 219 | | --- | --- | --- | --- | --- | --- | |
| 220 | | M1 | What an operation is | "create, push, pull, clone"; metrics events `create`, `fork`, `push`, `pull`, `delete` | `git_ops.rs` bills workspaces per upload-pack or receive-pack POST, including `ls-refs` answered from our cache; binding reads assumed free | Cost could be about 15× the plan; customers billed for operations that never reach Artifacts | R1 | |
| 221 | | M2 | Fork storage | Not documented; fork returns `objects: N` | `store.rs` calls forks "copy-on-write"; forks never deleted until the parent is purged | 1 TB account limit; storage grows forever | R2 | |
| 222 | | M3 | Namespace rate | 2,000 per 10 s per namespace | One namespace for all repositories and forks | 429s across all of g1t at peak | R7 | |
| 223 | | M4 | Repository size | 1 GB | Free private storage is 1 GB per workspace; no per-repository check; imports capped at 40 MB | Late `storageLimitReached` failures with no explanation | R4 | |
| 224 | | M5 | File size | 32 MB | No check before forwarding | Push fails inside Artifacts | R4 | |
| 225 | | M6 | Push bodies | Workers 128 MB per isolate; 100 MB body cap on our zone plan | `git_http::forward` reads the body (`request.bytes()`) and copies it into a `Uint8Array`; push protection also reads it | Pushes above roughly 40–50 MB can exceed isolate memory, taking concurrent requests with them | R4 | |
| 226 | | M7 | Landing and mirrors | Fetch streams | `land::fetch_pack` buffers the pack, `unpack_sideband` copies it, `push_pack` copies again | Large pull requests can fail to land | R4 | |
| 227 | | M8 | Token lifetime | 60 s to 1 year | 300 s, reused 180 s | Mint load and 0.8 s latency on each miss | R3 | |
| 228 | | M9 | `info()` | "Each call performs a fresh lookup" | Called on every mint only to read `remote` | One wasted call per mint; `remote` is fixed per key | R3 | |
| 229 | | M10 | Partial clone | v1 `filter` unsupported | v2 `blob:none` works in production | Building on undocumented behaviour | Q3 | |
| 230 | | M11 | Read-after-write | Not documented | `record_push` reads `log(branch, 1)` right after the push response; `refs_moved` assumes refs are final when the response ends | Missed push events if a read lags | Q5 | |
| 231 | | M12 | Errors | Typed `ArtifactsError`, `rateLimited` events | `ALREADY_EXISTS`/`NOT_FOUND` tolerated; everything else returns 500; no retry, no breaker | Brief Artifacts errors become user-visible failures | R5 | |
| 232 | | M13 | Durability | Synchronous replication, asynchronous snapshots; no SLA; no export | No copy outside Artifacts | Beta incident or account issue with no recovery path | R11 | |
| 233 | | M14 | Data location | Jurisdiction per namespace, fixed | `PLAN.md` promises residency per workspace; only `g1t` exists | EU customers cannot be offered residency | R7 | |
| 234 | | M15 | Direct credentials | Tokens are bearer, repo-scoped | `git_access` hands out raw write tokens; pushes with them skip branch protection and push protection (`refs_open` only stops caching) | Policy bypass if a token leaks out of a sandbox | Keep TTL minimal on this path | |
| 235 | | M16 | Hot repository | 2,000 git requests per 10 s per repository; DO soft limit 1,000 req/s | Agents clone the same repository many times per pull request | Not near the limit today; a monorepo with many agents could be | R6 | |
| 236 | |
| 237 | ## 5. Capacity model: 3,000 workspaces |
| 238 | |
| 239 | ### Assumptions |
| 240 | |
| 241 | - 3,000 workspaces, 10,000 repositories, 6,000 active people, average repository 25 MB stored. |
| 242 | - People: 6 fetches and 3 pushes per person per day. |
| 243 | - Agent pull requests: 15,000 a day (5 per workspace). Human pull requests: 3,000 a day. |
| 244 | - Per agent pull request: 1 fork; 1.5 agent runs × (clone + upstream fetch + push); 1.5 check clones; |
| 245 | review clone + fetch; mergecheck in 30% of cases (clone + fetch); 0.5 catch-up push; merge queue clone + |
| 246 | fetch; landing upload-pack + receive-pack; 0.3 deploy clones. About **14 git operations**. |
| 247 | - Actions: 1.5 checkouts per push. Mirrors: 1,000 repositories × 24 syncs × 2. |
| 248 | - Web: 60 page views per person per day, about 5 uncached binding calls each. |
| 249 | - Mergeability: 18,000 default-branch moves a day × 8 open pull requests × about 20 binding calls. |
| 250 | |
| 251 | ### Operations per day |
| 252 | |
| 253 | | Source | Scenario A: git data operations only | Scenario B: every Artifacts call | |
| 254 | | --- | ---: | ---: | |
| 255 | | People: fetch and push | 54,000 | 54,000 | |
| 256 | | Agent pull requests (15,000 × 14) | 210,000 | 210,000 | |
| 257 | | Human pull requests (3,000 × 8) | 24,000 | 24,000 | |
| 258 | | Actions checkouts | 60,000 | 60,000 | |
| 259 | | Mirrors | 48,000 | 48,000 | |
| 260 | | Credential mints (≈ 200,000 × 3) | 0 | 600,000 | |
| 261 | | Ref listing misses (`info/refs`, `ls-refs`) | 0 | 300,000 | |
| 262 | | Web pages (360,000 × 5) | 0 | 1,800,000 | |
| 263 | | Mergeability (18,000 × 8 × 20) | 0 | 2,900,000 | |
| 264 | | Search indexing, push protection, `record_push` | 0 | 900,000 | |
| 265 | | **Total per day** | **≈ 400,000** | **≈ 6,900,000** | |
| 266 | | **Per month** | **≈ 12 M** | **≈ 207 M** | |
| 267 | | **Cost per month at $0.15 / 1,000** | **≈ $1,800** | **≈ $31,000** | |
| 268 | |
| 269 | Rates: scenario A averages 5 operations per second. Scenario B averages 80 calls per second and peaks |
| 270 | around 250–400, above the 200 per second a single namespace allows, if binding calls count toward it. |
| 271 | |
| 272 | Hot spots, largest first in scenario B: mergeability fan-out (42%), web reads (26%), credential mints |
| 273 | (9%), indexing and scanning (13%), sandbox clones (6%, but they carry almost all the bytes and the |
| 274 | latency). |
| 275 | |
| 276 | ### Storage |
| 277 | |
| 278 | | Item | Estimate | Cost per month | |
| 279 | | --- | --- | ---: | |
| 280 | | 10,000 repositories × 25 MB | 250 GB | ≈ $125 | |
| 281 | | Pull request forks if shared (about 0.5 MB of new objects each) | +270 GB every month, never deleted | +$135, growing each month | |
| 282 | | Pull request forks if copied (25 MB each, 540,000 a month) | +13.5 TB a month | ≈ $6,750 the first month, and the 1 TB account limit is reached in about 2 days | |
| 283 | |
| 284 | ### Free tier exposure |
| 285 | |
| 286 | `GIT_OPERATIONS_FREE_CAP` is 50,000 operations per workspace per month, never charged. At $0.15 per 1,000 |
| 287 | that is at most $7.50 per free workspace, or $22,500 a month if all 3,000 used it. Realistic use is far |
| 288 | lower, but the cap should be sized to what Cloudflare actually counts (R1). |
| 289 | |
| 290 | ## 6. Recommendations, ranked by risk × effort |
| 291 | |
| 292 | P0 means before 2026-10-14 or before agent volume ramps. Effort assumes one engineer. |
| 293 | |
| 294 | | # | Priority | What to build | Where | Expected effect | Effort | |
| 295 | | --- | --- | --- | --- | --- | --- | |
| 296 | | R1 | P0 | Find out what counts. Query `artifactsEventsAdaptiveGroups` grouped by `eventType` for the last 31 days and compare with `git_operations`; ask Cloudflare in writing. Then make `git_ops::count` count what Cloudflare counts (skip `ls-refs` and cache hits if they are free). | `services/repos/src/git_ops.rs`, a script under `scripts/` | Removes the 15× uncertainty; billing matches cost | 0.5 day plus Cloudflare's answer | |
| 297 | | R2 | P0 | Delete pull request forks after merge or close plus a grace period (for example 14 days), and verify fork storage semantics with a 100 MB test repository and the storage metric. | `lifecycle.rs` sweep (`23 * * * *`), `work` events `pull.merged`/`pull.closed` | Storage proportional to open work instead of all history; removes the 1 TB cliff | 1–2 days | |
| 298 | | R3 | P0 | Mint less and faster: TTL 3,600 s with a 50 min reuse window for credentials that never leave the service; keep 300 s for `git_access`. Derive `remote` from the key (`https://<account>.artifacts.cloudflare.net/git/g1t/<key>.git`) or keep it in the registry `store` column instead of calling `info()`. Retry a 401 once with a fresh token (already done for GET). | `store.rs` `TOKEN_TTL_SECONDS`, `TOKEN_REUSE_MS`, `ArtifactsRepo::access` | Mints about 15× fewer; cold git requests about 0.5 s faster | 0.5 day | |
| 299 | | R4 | P0 | Enforce limits in front of Artifacts: refuse objects over 32 MB and pushes that would take a repository over about 950 MB, with a git `ng` line (same framing as `declined`); stream the push body to Artifacts instead of buffering it twice, keeping only the command section and at most `MAX_SCANNED_PUSH` for scanning; stream `land` packs straight from upload-pack into receive-pack. | `git_http.rs` `forward`, `secret_scan.rs`, `land.rs` | Clear errors instead of late failures; no isolate OOM on 50–100 MB pushes | 2–3 days | |
| 300 | | R5 | P0 | Resilience: map `ArtifactsError.code` and HTTP 429/5xx to retry with jittered backoff (reads, mints, ref listings only; never a receive-pack), a per-isolate circuit breaker, `Retry-After` to git, and an "Artifacts" component on status.g1t.sh fed by the GraphQL error metrics (`rateLimited`, `serverError`, `storageLimitReached`). | `store.rs`, `git_http.rs`, `apps/status` | Brief Artifacts errors stop reaching users; incidents are visible | 2 days | |
| 301 | | R6 | P1 | Pack cache for full clones: when an upload-pack request has wants and no haves, key it by (repo id, `refs_version`, hash of the request) and serve the pack from R2; fill it on a miss with a tee of the response. Sandboxes clone the same commit repeatedly per pull request. | `git_http.rs`, `refs_cache.rs` pattern, new R2 bucket | Removes about 2 s of pack building per repeat clone and a large share of `pull` operations | 3–4 days | |
| 302 | | R7 | P1 | Shard namespaces: bindings `ARTIFACTS_0…N` plus `ARTIFACTS_EU`; record each repository's namespace in the registry `store` column (already read by `remember_store`); new repositories and forks go to the least-loaded shard; forks in their own namespaces. | `services/repos/wrangler.jsonc`, `store.rs`, `registry.rs` | Multiplies the control-plane ceiling; enables EU residency | 3 days | |
| 303 | | R8 | P1 | Sandboxes clone less: `--depth=1` for checks and deploy; `--filter=blob:none` only where lazy blob fetches are few (review of a small diff); keep full clones for agent runs and merges. Or restore a cached sandbox snapshot and `git fetch` (Sandbox SDK backups). | `crates/runner/src/*.rs` clone calls | Measured: 3.8 s (depth 1) and 3.4 s (blobless) against 5.4 s for g1t; fewer bytes from Artifacts | 1–2 days | |
| 304 | | R9 | P1 | Cache mutable reads by `refs_version`: `log(branch, n)`, `branches()` (parse the kept `refs_cache` advertisement instead of a new `info/refs`), and `readFile` (resolve the path through cached trees, then a cached `readBlob`). | `store.rs`, `refs.rs`, `lib.rs` `tree`/`blob`/`branches` | Most web page views stop reaching Artifacts | 2 days | |
| 305 | | R10 | P1 | Calm mergeability: coalesce re-checks per repository (one pass per target head, at most once a minute), resolve the target head once and `log` by hash (already cached), and cache `readCommit` like other objects. | `services/work/src/mergeability.rs`, `lib.rs` `divergence`, `descends_from` | The largest scenario-B hot spot drops by an order of magnitude | 1–2 days | |
| 306 | | R11 | P2 | Back up every repository to R2: a nightly incremental `git bundle` per repository whose `refs_version` changed, made in a Container (Workers cannot run git), with a restore drill. | new job in `services/runner` or a Container | Durability independent of the beta; also the export path | 3–5 days | |
| 307 | | R12 | P2 | Make the git store a real fallback: run `deploy/self-host/gitstore` on a host with persistent disk (Containers' disk is ephemeral and at most 20 GB), restore from the R2 bundles, and switch `GitStore` per namespace shard. | `deploy/self-host/gitstore`, `store.rs` | A tested exit path; not a hot standby | 1–2 weeks | |
| 308 | | R13 | P2 | Take the operation counter off the request path: record the count in `waitUntil` (or Analytics Engine), and check limits against a value cached for a minute. | `lib.rs` `git_limits`, `git_ops.rs` | Saves 63–98 ms on every fetch and push; removes a hot D1 row per workspace-hour | 1 day | |
| 309 | | R14 | P2 | Try Smart Placement or a placement hint for `g1t-repos`, which makes several sequential calls to Artifacts per request; compare `Server-Timing` before and after. | `services/repos/wrangler.jsonc` | Possibly lower latency for distant users | 0.5 day | |
| 310 | | R15 | P3 | Large files: an LFS endpoint backed by R2 for files over 32 MB. | new route in `services/repos` | Repositories with binaries can move to g1t | 1–2 weeks | |
| 311 | |
| 312 | ## 7. Questions for Cloudflare |
| 313 | |
| 314 | Ask in the Artifacts beta channel (the [beta request form](https://forms.gle/DwBoPRa3CWQ8ajFp7) is the |
| 315 | listed contact), through our account team, and in the Cloudflare Developers Discord. As a git |
| 316 | competition entrant (due 2026-10-14) we can also ask the competition organisers. Get answers in writing. |
| 317 | |
| 318 | 1. Exactly what is billed as an operation? Is each `upload-pack` and `receive-pack` request one, or each |
| 319 | clone or fetch? Do `info/refs`, `ls-refs`, binding calls (`get`, `info`, `createToken`, `log`, |
| 320 | `readTree`, `readBlob`, `readCommit`, `readFile`) or token creation count? |
| 321 | 2. Does `fork` share objects with its source or copy them? How is a fork's storage billed? Does |
| 322 | `objects` in the fork response mean objects copied? |
| 323 | 3. Is protocol v2 `filter` (partial clone) supported, given it works today but the docs say it does not? |
| 324 | 4. Do binding calls count toward "2,000 requests per 10 seconds per namespace"? What happens past it |
| 325 | (429, `rateLimited`, queueing)? Can it be raised per namespace? |
| 326 | 5. Read-after-write: once a receive-pack response has ended, are refs visible to `log`, `info/refs` and |
| 327 | other locations at once? |
| 328 | 6. Is `receive-pack` with `atomic` supported for multi-ref pushes? |
| 329 | 7. Where does a repository's Durable Object live, can it move, and can we hint a region per repository? |
| 330 | 8. What SLA, support path and incident communication apply during open beta? When is GA? |
| 331 | 9. Is there an export or snapshot restore for a deleted or corrupted repository? How long are R2 |
| 332 | snapshots kept? |
| 333 | 10. Can the 1 GB per repository and 32 MB per file limits be raised, and what does a client see when a |
| 334 | push crosses them? |
| 335 | 11. Does Artifacts garbage-collect or repack? Do deleted branches and force pushes free storage, and when? |
| 336 | 12. Can the 1 TB account limit be raised now, ahead of launch? |
| 337 | 13. Are per-repository event subscriptions practical at tens of thousands of repositories, or is an |
| 338 | account-level `pushed` event planned? |
| 339 | |
| 340 | ## 8. Launch blockers |
| 341 | |
| 342 | - **Before 2026-10-14:** R1 (know what is billed and count the same thing) and R2's verification (fork |
| 343 | storage semantics). If forks copy objects, R2's deletion is a launch blocker for agent-heavy |
| 344 | workspaces, because the 1 TB account limit would stop every push. |
| 345 | - **Before agent volume ramps (about 1,000 pull requests a day):** R2 deletion, R3, R4, R5. |
| 346 | - **Before a few thousand active workspaces:** R6, R7, R9, R10. |
| 347 | - Everything else is resilience and exit planning, ideally done while the product is still invite-only. |