Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1 | # Cloudflare Artifacts: due diligence for g1t at launch scale |
| 2 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 3 | Status: research document, 2026-10-06; R1–R5, R9, R10, R13 and R7 groundwork were built the same day (section 9). |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 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. | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 348 | |
| 349 | ## 9. What was built (2026-10-06) | |
| 350 | ||
| 351 | Code in `services/repos` unless named; one migration, | |
| 352 | `migrations/0011_artifacts_meters_forks_health.sql` (new columns on `repos`, new tables | |
| 353 | `artifacts_meters`, `operation_mapping`, `store_health`; additive, no backfill). | |
| 354 | ||
| 355 | | # | Status | What | | |
| 356 | | --- | --- | --- | | |
| 357 | | R1 | Built; Cloudflare's answer still needed | Every interaction with the store is metered raw (`meters.rs` → `artifacts_meters`, per day, namespace, repository, workspace and meter, with bytes where known): client git (`git.info_refs`, `git.ls_refs`, `git.fetch`, `git.receive_pack`), g1t's own git (`internal.git.*`: landing, catch-up, mirrors, branch listings, fork retirement), every binding call (`binding.get`, `binding.create_token`, `binding.log`, `binding.read_tree`, …, each retry included), and answers g1t served from its own cache (`cache.*`, never operations). Which meters are operations is data: `operation_mapping` (`cost_operations` for g1t's bill, `billable_operations` for workspaces), read every 5 minutes, changed with `set_operation_mapping` without a deploy. Default: `git.fetch`, `git.receive_pack`, `internal.git.fetch`, `internal.git.receive_pack`, `binding.create`, `binding.fork`, `binding.delete` = 1, everything else 0. `git_operations` (what billing reads) is filled from the meters × `billable_operations`, by the hour. RPCs: `artifacts_usage { from, to, workspace?, by_repo? }` (raw meters and the mapping, for the reconciler), `operation_mapping`, `set_operation_mapping { meter, cost_operations, billable_operations, note? }`. Script: `scripts/ops/artifacts-usage.mjs`. | | |
| 358 | | R13 | Built | Nothing on the request path writes D1 for counting. Meters add up per isolate and are written in one batch from `ctx.wait_until` after every request (and at the end of the cron and queue handlers); a failed write is kept for the next. The free-workspace slow-down decides from counts the isolate read back after its last write plus what it added since (`git_ops::standing`, at most 10 minutes old) and billing's plan answer kept 5 minutes. The 63–98 ms `kept` step's D1 upsert is gone. A workspace whose counts this isolate never read is not slowed: nothing slows anyone on a guess. | | |
| Pull request forks are removed a day after they merge or close, not a week | 359 | | R2 | Built; the fork storage test is yours to run | `pull.merged` and `pull.closed` set the fork's `retire_after` (`FORK_RETENTION_DAYS`, 1 day in production; 7 when unset); `pull.reopened` clears it, or makes the fork again. The hourly sweep (`23 * * * *`, 25 a run) keeps the fork's head in its repository as `refs/pull/<pull id>/head` (only missing objects travel; an empty pack when merged), records `retired_at` and `retired_head`, then deletes the fork from the store (a failed delete puts the row back). Reads of a retired fork (the pull request's changes, divergence, tree, blob, log, branches) are answered from the repository with the fork's branch mapped to the kept head (`forks.rs` `Viewed`). Anything that writes or uses git on it (git over HTTPS, `git_access`, catch-up, land, `delete_branch`) makes it again first (`revive`: fork, then move its branch to the head) and schedules it to go again. Work never emits `pull.reopened` today; the handler is ready for it. Script: `scripts/ops/fork-storage-test.mjs`. | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 360 | | R3 | Built | Credentials g1t uses itself: TTL 3,600 s, reused for 50 minutes (isolate and KV, key `cred2:<key>:<scope>:internal`). `git_access` hands out its own: TTL 300 s, reused 180 s (`…:handout`); `refs_open` still uses 300 s. Every internal path (land, catch-up, mirrors, branch listing, commits, deleting a branch) now reuses kept credentials instead of minting each time. The remote is worked out as `https://<account>.artifacts.cloudflare.net/git/<namespace>/<name>.git` (the documented format, `api/git-protocol`), learned per namespace from the first `info()` an isolate makes, which runs alongside `createToken` and so costs no time; after that a mint is `get` and `createToken`. Optional `ARTIFACTS_REMOTE_BASE` skips even the first `info()`. | |
| 361 | | R4 | Built | Pushes are read as they arrive (`request.stream()`) and walked by `pack_limits::PackSizer` (each object inflated into a 32 KiB window and thrown away): an object over 32 MB (a delta measured by the object it makes), or a push taking the repository and its forks (`stored_bytes`) past `REPO_STORAGE_LIMIT_BYTES` (950 MB), is declined with `ng` lines and `remote:` text; a repository already at the limit is refused at the push's `info/refs` in plain text. Up to 24 MiB is kept, scanned and sent on as one copy, not three. Past 24 MiB push protection cannot read the push, so it is declined (`LARGE_PUSHES=refuse`, failing closed) with a command to push in parts, the 100 MB network limit named; `LARGE_PUSHES=unscanned` streams it to the store instead, still size-checked (a violation ends the stream before the pack's checksum, so the store keeps nothing). A pack too large for the scanner to inflate (48 MB inflated) is declined the same way instead of let through. Landing streams: upload-pack's side-band answer is taken apart chunk by chunk (`pack_limits::Sideband`) straight into the receive-pack body. | | |
| 362 | | R5 | Built | `resilience.rs` sorts errors into rate limited, transient (`INTERNAL_ERROR`, `UPSTREAM_UNAVAILABLE`, `*_IN_PROGRESS`, no code, HTTP 5xx) and permanent. Binding reads, `get`, `info`, `createToken`, `create` and `delete` try up to 3 times with exponential backoff and jitter (80 ms base, 400 ms for rate limits, 2 s cap); `fork` and every receive-pack never retry. Git reads (`info/refs`, upload-pack) retry on 429 and 5xx. Per isolate, each namespace has a breaker that opens after 5 transient failures in a row, for 10 s, then lets one probe through. Busy answers reach git as 429 (rate limited) or 503, with `Retry-After: 5`; the site's read RPCs (`tree`, `blob`, `log`, `branches`, `blame`, `compare`) answer an `Outcome` failure saying the git storage is busy; other RPCs answer 503 with the same words. Health is counted by the minute (`store_health`) and served by the `store_health { minutes }` RPC; status.g1t.sh lists **Git storage** through a new `REPOS` service binding (down: 25% or more of at least 5 calls failed, or the breaker refused calls; degraded: rate limited, or a mean call over 1.5 s). | | |
| 363 | | R9 | Built | `log(branch)`, `branches()` and `read_file(ref, path)` are kept in the colo cache under the repository's `refs_version` (5 minutes at most, and only while `refs_cache::usable`), and by commit hash for good; a log by branch also fills the by-hash entry; `readCommit` (parents) is kept for good. Read RPCs open repositories through `read_git`, which sets the version. | | |
| 364 | | R10 | Built in repos; work unchanged | `divergence` works out the target's side once per target head per isolate (`coalesce.rs`: the head under the refs version, then the history by hash, kept 60 s), and what the target changed between two trees once per pair (10 minutes). `readCommit` and logs by hash come from the cache. Work's fan-out (`after_push`, up to 100 pull requests) is unchanged: its 100 `divergence` calls now cost one walk of the target instead of 100. | | |
| 365 | | R7 | Groundwork | `shards.rs`: bindings named in `ARTIFACTS_NAMESPACES` (JSON, binding → namespace; `ARTIFACTS` → `g1t` always there), a repository's namespace kept in its `store` column as `<namespace>/<key>` (no prefix means the `ARTIFACTS` namespace, so every existing key reads the same), new repositories placed by `ARTIFACTS_NEW_REPOS` (comma-separated, spread by an FNV hash of the repository id; names not bound are skipped), forks always in their repository's namespace, `ARTIFACTS_EU_NAMESPACE` reserved for EU residency (no workspace setting yet). Works with only `ARTIFACTS` bound, as today. | | |
| ARTIFACTS.md: R8 (shallow sandbox clones) is built | 366 | | R8 | Built | `crates/runner/src/clone.rs`: every sandbox clones at `--depth=1` (a full g1t clone took 5.4 s, depth 1 took 3.8 s). Work that merges (catch-up, the merge queue, merge checks, a review's diff) deepens 50, 500, then 5000 commits until the two sides share one, and fetches everything only as the last resort (`share_history`). `G1T_CLONE_DEPTH` (0 or `full` for everything) and `G1T_CLONE_FILTER=blob:none` change it per runner. | |
| Keep fresh clones' packs in R2, so a repeat clone of the same commit skips the git store | 367 | | R6 | Built; the bucket must exist before it deploys | `pack_cache.rs`: an upload-pack POST with wants and no `have` or `shallow` lines (a fresh clone, the sandboxes' `deepen 1` ones included), uncompressed and at most 1 MiB, is keyed `packs/<repo id>/<refs_version>/<sha256>` over the request normalized: protocol v2 capabilities without `agent=`/`session-id=` and its arguments, each sorted and deduplicated; v0/v1 wants sorted, the first want's capabilities split off, sorted and without `agent=`, then `deepen`/`filter` lines, a flush and `done`. Only while `refs_cache::usable` (the version known, and no push credential out of g1t's hands), so never across a refs change. Looked up after authorization, alongside the free-workspace limits and the kept refs answer; a hit streams from the bucket (`Server-Timing` `pack;desc=hit`). A miss streams the store's 200 to git through a tee that copies it to a fill in `ctx.wait_until` (at most 5 MiB queued between them, 2 fills per isolate, one per key): under 5 MiB it is one `put` once it all arrived; larger, 5 MiB multipart parts completed only after the last part and a check that it is one whole side-band pack (well-formed pkt-lines, `PACK` on channel 1, no `ERR` or channel 3, a closing flush). Over 200 MB, a queue that falls behind, git going away or the store's stream failing lets the fill go and aborts the upload; nothing partial can be read. Meters `pack_cache.hit` (with the bytes served) and `pack_cache.miss` (counted with `record`, bytes added at the end), neither an operation by default; a hit records no `git.fetch`. Storage is behind the `PackStore` port with an R2 adapter (`GIT_PACKS`, bucket `g1t-git-packs`, lifecycle: packs deleted after 7 days, unfinished uploads after 1); without the binding (self-hosted) nothing is kept. | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 368 | |
| 369 | ### R1: reading `scripts/ops/artifacts-usage.mjs` | |
| 370 | ||
| 371 | ```sh | |
| 372 | export CLOUDFLARE_API_TOKEN=<token with Account Analytics: Read (and D1: Read, or set CLOUDFLARE_D1_TOKEN)> | |
| 373 | node scripts/ops/artifacts-usage.mjs # last 31 days, a table | |
| 374 | node scripts/ops/artifacts-usage.mjs --days 7 --json > usage.json | |
| 375 | ARTIFACTS_NAMESPACE=g1t node scripts/ops/artifacts-usage.mjs | |
| 376 | ``` | |
| 377 | ||
| 378 | It prints, per day, Cloudflare's `pull`, `push`, `create`, `fork` and `delete` events and its | |
| 379 | errors beside g1t's fetch and push meters and `git_operations`, then, for each Cloudflare event, | |
| 380 | the ratio Cloudflare ÷ g1t for several combinations of meters. Read it like this: | |
| 381 | ||
| 382 | - `pull` ≈ `git.fetch + internal.git.fetch` (ratio 1.00): Cloudflare counts one pull per | |
| 383 | upload-pack fetch, as assumed. Keep the default mapping. | |
| 384 | - `pull` ≈ a combination with `git.ls_refs` or `git.info_refs`: listing refs counts too. Set | |
| 385 | `cost_operations` for those meters to 1 (`set_operation_mapping`), and decide whether | |
| 386 | `billable_operations` follows (cost pass-through says yes). | |
| 387 | - Every ratio well under 1: Cloudflare counts per clone or fetch session, not per request. | |
| 388 | Ratios over 1: something reaches Artifacts that g1t does not meter, such as sandboxes pushing | |
| 389 | directly with handed-out credentials. | |
| 390 | - Only days after the meters were deployed compare; before that only `git_operations` exists. | |
| g1t-runner 0.1.0 is released: signed binaries for five platforms at g1t.sh/downloads/runner; the Artifacts checks' results | 391 | - Binding calls do appear: Cloudflare's events include `read` and `token_create` actions (and |
| 392 | `namespace_*`) besides the five documented ones. If Cloudflare says they are billed, map the | |
| 393 | `binding.*` meters in `operation_mapping`. | |
| 394 | ||
| 395 | A global API key works in place of the token: `CLOUDFLARE_API_KEY` with `CLOUDFLARE_EMAIL` | |
| 396 | (both scripts). | |
| 397 | ||
| 398 | **Result, 2026-10-06** (31 days; g1t's meters cover only 2026-10-06, the day they shipped): | |
| 399 | ||
| 400 | | Cloudflare event | 31 days | 2026-10-06 | g1t's meters, 2026-10-06 | | |
| 401 | | --- | --- | --- | --- | | |
| 402 | | `read` | 137,225 | 107,616 | `binding.get` 85,206, `read_file` 49,846, `read_tree` 9,208, `read_blob` 4,964, `log` 2,798 | | |
| 403 | | `pull` | 646 | 101 | `git.fetch` 29, `git.ls_refs` 26, `git.info_refs` 426 (+ 129 internal) | | |
| 404 | | `push` | 385 | 10 | `git.receive_pack` 3 | | |
| 405 | | `token_create` | 2,721 | 338 | `binding.create_token` 34 | | |
| 406 | | `fork` / `create` / `delete` | 96 / 14 / 8 | 2 / 0 / 0 | | | |
| 407 | | errors | 699 (688 client) | 476 client | | | |
| 408 | ||
| 409 | - `pull` is not one per upload-pack fetch: 101 pulls against 29 fetches on the one day both | |
| 410 | exist. Over 31 days `pull` ≈ fetch + ls-refs + info/refs (ratio 1.06), so listing refs likely | |
| 411 | counts as a pull. Not yet changed in `operation_mapping`: one day of meters is too little, and | |
| 412 | re-check after a week before setting `cost_operations` for `git.info_refs` and `git.ls_refs`. | |
| 413 | - `read` is the open question that matters. If reads are billed as operations at $0.15 per | |
| 414 | 1,000, today's demo-scale traffic alone is about 3.2 million a month (~$480). Ask Cloudflare | |
| 415 | (Q1) before 2026-10-14. Either way the volume is mostly waste: every repos call opens a handle | |
| 416 | with `get` even when the answer is cached, and the object cache may not be hitting (no hit/miss | |
| Fewer Artifacts reads: the store is asked for a handle only when needed, objects are kept in the isolate, and issue events read no workflows nobody listens for | 417 | meter yet). Done on 2026-10-06: the handle's `get` waits for the first call that needs the |
| 418 | store (an answer from a cache, or `branches` over git, costs none); objects named by hash are | |
| 419 | kept in the isolate (16 MB, oldest out first) ahead of the Cache API, and every look is | |
| 420 | metered (`cache.memory_hit`, `cache.edge_hit`, `cache.miss`); issue and comment events start | |
| 421 | nothing and read nothing when the synced `workflows` table has no workflow listening. Next: | |
| 422 | read `cache.edge_hit` against `cache.miss` after a day; if the Cache API never hits from a | |
| 423 | Worker reached only by service bindings, put objects in KV instead. Still to do: caller | |
| 424 | attribution in the meters. | |
| g1t-runner 0.1.0 is released: signed binaries for five platforms at g1t.sh/downloads/runner; the Artifacts checks' results | 425 | - 476 client errors on 2026-10-06 are unexplained; the fetch fix below accounts for some (every |
| 426 | failed negotiation was one). | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 427 | |
| 428 | ### R2: running and reading `scripts/ops/fork-storage-test.mjs` | |
| 429 | ||
| 430 | ```sh | |
| 431 | export CLOUDFLARE_API_TOKEN=<token: Artifacts edit, Account Analytics read> | |
| 432 | node scripts/ops/fork-storage-test.mjs schema # which Artifacts analytics datasets exist | |
| 433 | node scripts/ops/fork-storage-test.mjs run --keep # namespace g1t-storage-test: 100 MB repository, 5 forks | |
| 434 | node scripts/ops/fork-storage-test.mjs measure # again tomorrow (storage is billed as a daily peak) | |
| 435 | node scripts/ops/fork-storage-test.mjs cleanup # delete the 6 repositories | |
| 436 | ``` | |
| 437 | ||
| 438 | It works only in its own namespace, through Cloudflare's REST API, never through g1t. Read: | |
| 439 | ||
| 440 | - Fork timing and response: a fork that returns in well under a second, with `objects` near the | |
| 441 | source's count, is metadata (sharing). Seconds per fork, growing with size, suggests copying. | |
| 442 | - Storage figures (any dataset `schema` lists besides `artifactsEventsAdaptiveGroups`): about | |
| 443 | 100 MB after the forks means sharing; about 600 MB means each fork copied. The documentation | |
| 444 | lists no storage dataset today, so this may print nothing: then the next day's usage in the | |
| 445 | dashboard (Billing → Artifacts storage) is the measure, and the question stays with Cloudflare (Q2). | |
| 446 | - `events`: `storageLimitReached` or other errors during the test. | |
| 447 | ||
| 448 | Either way R2 retires forks; the answer decides `FORK_RETENTION_DAYS` (shared: a week is fine; | |
| 449 | copied: shorten it to 1 or 2 days and ask Cloudflare to raise the 1 TB account limit). | |
| 450 | ||
| g1t-runner 0.1.0 is released: signed binaries for five platforms at g1t.sh/downloads/runner; the Artifacts checks' results | 451 | **Result, 2026-10-06: forks are stored and billed as copies.** A 100 MB source took 57 s to |
| 452 | push; each of 5 forks took 4–6 s. The storage dataset (`artifactsStorageAdaptiveGroups`, | |
| 453 | `max.repositorySizeBytes`) gave every fork the source's full 105,582,592 bytes: about 633 MB for | |
| 454 | the six, not about 106 MB. Whatever Artifacts shares underneath, storage billing and the 1 TB | |
| 455 | account limit see full copies. So: | |
| 456 | ||
| Pull request forks are removed a day after they merge or close, not a week | 457 | - `FORK_RETENTION_DAYS` is 1 in production (changed 2026-10-07). |
| g1t-runner 0.1.0 is released: signed binaries for five platforms at g1t.sh/downloads/runner; the Artifacts checks' results | 458 | - g1t meters a workspace's storage once per repository (`stored_bytes`), so an open pull |
| 459 | request's working copy is Cloudflare cost g1t absorbs: about $0.05 a month per 100 MB per open | |
| 460 | pull request. Small now; decide whether open working copies count toward a workspace's | |
| 461 | storage before agent pull requests reach thousands. | |
| 462 | - Ask Cloudflare whether forks share objects physically, and for a higher account limit. | |
| 463 | ||
| 464 | Also found while testing (fixed in `git_http.rs`): the store answers a protocol v2 fetch that is | |
| 465 | still negotiating, and whose `have`s it does not know, with `acknowledgments`, `NAK`, then a pack. | |
| 466 | git refuses that ("expected no other sections to be sent after no 'ready'"). g1t now ends such | |
| 467 | an answer after the acknowledgments with a flush, and the client negotiates again. Report it to | |
| 468 | Cloudflare. | |
| 469 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 470 | ### R7: making more namespaces (yours to run, when needed) |
| 471 | ||
| 472 | ```sh | |
| 473 | # A US shard, unrestricted like today's g1t, and an EU one. | |
| 474 | curl -X POST "https://api.cloudflare.com/client/v4/accounts/1e6f2cffa3f445920836e8ebe446bb58/artifacts/namespaces" \ | |
| 475 | -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" \ | |
| 476 | --data '{"namespace":"g1t-us-1"}' | |
| 477 | curl -X POST "https://api.cloudflare.com/client/v4/accounts/1e6f2cffa3f445920836e8ebe446bb58/artifacts/namespaces" \ | |
| 478 | -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" \ | |
| 479 | --data '{"namespace":"g1t-eu","jurisdiction":"eu"}' | |
| 480 | ``` | |
| 481 | ||
| 482 | Then in `services/repos/wrangler.jsonc`: | |
| 483 | ||
| 484 | ```jsonc | |
| 485 | "artifacts": [ | |
| 486 | { "binding": "ARTIFACTS", "namespace": "g1t" }, | |
| 487 | { "binding": "ARTIFACTS_1", "namespace": "g1t-us-1" }, | |
| 488 | { "binding": "ARTIFACTS_EU", "namespace": "g1t-eu" } | |
| 489 | ], | |
| 490 | "vars": { | |
| 491 | "ARTIFACTS_NAMESPACES": "{\"ARTIFACTS\":\"g1t\",\"ARTIFACTS_1\":\"g1t-us-1\",\"ARTIFACTS_EU\":\"g1t-eu\"}", | |
| 492 | "ARTIFACTS_NEW_REPOS": "g1t,g1t-us-1", // new repositories spread over both | |
| 493 | "ARTIFACTS_EU_NAMESPACE": "g1t-eu" // used once a workspace can choose the EU | |
| 494 | } | |
| 495 | ``` | |
| 496 | ||
| 497 | Existing repositories stay where they are (`store` without a prefix). Deploy the binding before | |
| 498 | naming its namespace in `ARTIFACTS_NEW_REPOS`; a name that is not bound is skipped, never used. | |
| 499 | Moving an existing repository between namespaces is not built (a clone and push, then a `store` | |
| 500 | update). | |
| 501 | ||
| 502 | ### Deploy order and what to watch | |
| 503 | ||
| 504 | 1. Migration 0011 (the deploy tool applies migrations first). `forks_of` reads `retired_at`, so | |
| 505 | the new code must not run before it. | |
| 506 | 2. `g1t-repos` (new vars in `wrangler.jsonc`; no new bindings). | |
| 507 | 3. `g1t-status` (a new `REPOS` service binding; **Git storage** appears once deployed). | |
| 508 | 4. After a day: `scripts/ops/artifacts-usage.mjs`; then the fork test. | |
| 509 | ||
| 510 | Expected: `Server-Timing` `kept` on POSTs drops from 63–98 ms to the KV and Cache lookups only (a | |
| 511 | few ms); `mint` happens about once per repository and scope every 50 minutes per isolate instead | |
| 512 | of every 3 minutes, and costs `get` and `createToken` (about 0.5 s instead of 0.8 s) once an | |
| 513 | isolate knows its namespace's prefix; branch pages and repeated tree and file views skip | |
| 514 | Artifacts while the refs version holds; a burst of 100 mergeability checks walks the target once. | |
| 515 | ||
| 516 | Risks: what Cloudflare bills is still theirs to confirm (the mapping makes changing it cheap). | |
| 517 | Counts held by an isolate that is evicted before its `wait_until` write are lost (seconds of | |
| 518 | traffic). Pushes over 24 MiB now fail closed: bringing a large existing repository needs the | |
| 519 | push-in-parts command (in `guides/git.md`). Moving a revived fork's branch back to its old head | |
| 520 | is a non-fast-forward update, which depends on Artifacts accepting it as git does by default; if | |
| 521 | it refuses, the fork of a closed pull request cannot be made again and the error says so. | |
| 522 | `refs/pull/*` refs appear in full ref advertisements (mirrors, `--mirror` clones). Operations on | |
| 523 | pull request forks are metered under the workspace `pulls`, so they are counted for g1t's bill | |
| 524 | but not charged to a workspace (except the fork itself, metered on its repository). |