Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Merge branch 'worktree-agent-a56293a6f836cd343'

syntaqxcommitted Parents778271dc320fc1Browse files
38 files+235−16022 files not counted0/38 viewed
+41−0
220220 `g1t cache usage` says what the repository's entries hold against its
221221 10 GiB, and `g1t cache list --sort size-in-bytes` which take the most.
222222
223+## Adding a machine size
224+
225+g1t's hosted machines are one catalogue: `INSTANCE_TYPES` in
226+`crates/contracts/src/actions.rs`, mirrored row for row in
227+`packages/contracts/src/instances.ts`. Each row is a `runs-on` label, the
228+machine it bills as (`machine`, such as `2-core`), the runner's Durable
229+Object class that runs it (`class` / `sandboxClass`), the Cloudflare
230+Containers instance type behind that class (`container`), its vCPUs,
231+memory and disk, and its price a minute (`micros_per_minute`). The runner
232+starts a job in its label's class, billing charges its minutes at its
233+rate and reports them by machine, and the docs' tables follow it.
234+
235+- **A new label for an existing machine** (another name for the standard
236+ one, say): a row with the new label and class and the machine's
237+ `machine`, size and rate; the class and its binding below.
238+- **A new size**: a row with a new `machine` key and its rate. The rate is
239+ what a minute costs at Cloudflare's list prices with half the vCPUs
240+ busy, plus 20%, rounded up to a tenth of a cent, and never under its
241+ cost with every vCPU busy; `rates_cover_the_machine_at_full_cpu_and_its_usual_cost_plus_the_margin`
242+ checks it.
243+- **Pointing a label at another instance type**: change its `container`
244+ and size (and `machine`, if it is a different machine now), and the
245+ class's `instance_type` in `services/runner/wrangler.jsonc`. A custom
246+ instance type works the same way, once the account may use them: add its
247+ size to the runner test's table of instance types.
248+- **Renaming a label**: change `label`; keep `machine`, so its minutes stay
249+ on the same line of everyone's usage.
250+
251+Never reuse a `machine` key for a different machine: past usage is
252+reported under it. For a new class, add it to `services/runner/wrangler.jsonc`
253+(`containers`, a `durable_objects` binding and a migration tag with
254+`new_sqlite_classes`), export it from `services/runner/src/index.ts`
255+(`export class SandboxXCore extends AttemptSandbox {}`), and give it a
256+binding in `SANDBOX_BINDINGS` (`services/runner/src/egress.ts`). The tests
257+that hold all of this together are `cargo test -p g1t-contracts instance`
258+(the table, its rates and its TypeScript mirror) and the runner's
259+`npm test` (every class in wrangler.jsonc, its instance type, size and
260+binding against the catalogue). Then update the machine tables in
261+`guides/actions.md`, `guides/usage-and-billing.md` and
262+`architecture/compute.mdx`.
263+
223264 ## Speed
224265
225266 Pages are a few rounds of service calls, and each round costs a trip to
+12−16
64006400 "from": "2026-10-01",
64016401 "until": "2026-10-07",
64026402 "totals": {
6403− "price_micros": 48210000,
6403+ "price_micros": 48212000,
64046404 "discount_micros": 0,
64056405 "included_micros": 20000000,
64066406 "credits_micros": 18000000,
6407− "charged_micros": 10210000,
6407+ "charged_micros": 10212000,
64086408 "pending_micros": 1340000,
64096409 "pending_charged_micros": 1340000,
64106410 "cost_micros": 40100000
64306430 {
64316431 "key": "sandboxes",
64326432 "label": "Sandboxes",
6433− "micros": 7210000,
6433+ "micros": 7212000,
64346434 "meters": [
64356435 {
6436− "key": "sandbox",
6437− "label": "Sandbox time",
6436+ "key": "runner_2-core",
6437+ "label": "Runner minutes, 2-core (2 vCPU, 8 GiB, 16 GB)",
64386438 "product": "sandboxes",
6439− "unit": "seconds",
6440− "quantity": 41300,
6441− "micros": 7210000,
6439+ "unit": "minutes",
6440+ "quantity": 1803,
6441+ "micros": 7212000,
64426442 "pending_micros": 0,
64436443 "daily": [
64446444 900000,
64476447 1100000,
64486448 0,
64496449 2100000,
6450− 1110000
6450+ 1112000
64516451 ],
6452− "allowance": {
6453− "used": 41300,
6454− "of": 108000,
6455− "unit": "seconds"
6456− },
6452+ "allowance": null,
64576453 "by_project": [
64586454 {
64596455 "project": "flagon-io/g1t",
6460− "micros": 7210000,
6461− "quantity": 41300
6456+ "micros": 7212000,
6457+ "quantity": 1803
64626458 }
64636459 ]
64646460 }
+1−1
319319 | Limit | Value |
320320 | --- | --- |
321321 | Largest machine | 4 vCPUs, 12 GiB of memory, 20 GB of disk (`g1t-4core`). No GPUs. |
322−| Disk | 20 GB on every machine, the most Cloudflare Containers give one. |
322+| Standard machine | 2 vCPUs, 8 GiB of memory, 16 GB of disk (`ubuntu-latest`, every other hosted label, and `g1t-2core`). |
323323 | One job on g1t's machines | 60 minutes. On a self-hosted runner, 24 hours. |
324324 | One cache entry | 2 GiB, compressed. A larger one is not saved. |
325325 | A repository's caches | 10 GiB together. Past it, the entries restored longest ago are removed. |
+10−9
6969
7070 ## Sandboxes on g1t's runners
7171
72−`runner` defines three Container classes, picked by `runs-on`:
72+`runner` defines three Container classes, picked by `runs-on`, each a
73+named Cloudflare instance type:
7374
7475 | `runs-on` | Class | Size |
7576 | --- | --- | --- |
76−| Any other hosted label, and every other kind of work | `AttemptSandbox` | 1 vCPU, 4 GiB memory, 20 GB disk (a custom instance type) |
77−| `g1t-2core` | `Sandbox2Core` | 2 vCPU, 8 GiB memory, 20 GB disk (a custom instance type) |
78−| `g1t-4core` | `Sandbox4Core` | 4 vCPU, 12 GiB memory, 20 GB disk (Cloudflare's `standard-4`) |
77+| Any other hosted label, and every other kind of work | `AttemptSandbox` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
78+| `g1t-2core` | `Sandbox2Core` | `standard-3`: 2 vCPU, 8 GiB memory, 16 GB disk |
79+| `g1t-4core` | `Sandbox4Core` | `standard-4`: 4 vCPU, 12 GiB memory, 20 GB disk |
7980
80−Each has the 20 GB of disk Containers allow at most. The named instance
81−types with 1 and 2 vCPU come with 8 and 16 GB, so those two are custom
82−instance types (`instance_type` with `vcpu`, `memory_mib` and `disk_mb` in
83−`services/runner/wrangler.jsonc`); 1 vCPU is the smallest a custom type may
84−have.
81+`g1t-2core` is kept as a name of the standard machine, so workflows that
82+ask for it keep working; it keeps its class and its own `max_instances`
83+in `services/runner/wrangler.jsonc`. The contracts' instance table
84+(`INSTANCE_TYPES`) and billing's price keeper say the same sizes, and a
85+test holds them to `wrangler.jsonc`.
8586
8687 Every sandbox starts from one image: `services/runner/base/Dockerfile`,
8788 which has Node, Python, Go, Rust, Java, .NET, Ruby and Docker, with the
+21−19
537537
538538 ### Machine sizes
539539
540−A job runs on the standard machine unless its `runs-on` names a larger
541−one:
540+A job runs on the standard machine unless its `runs-on` is `g1t-4core`:
542541
543−| `runs-on` | vCPUs | Memory | Disk |
544−| --- | --- | --- | --- |
545−| `ubuntu-latest`, or any other Linux label | 1 | 4 GiB | 20 GB |
546−| `g1t-2core` | 2 | 8 GiB | 20 GB |
547−| `g1t-4core` | 4 | 12 GiB | 20 GB |
542+| `runs-on` | vCPUs | Memory | Disk | A minute |
543+| --- | --- | --- | --- | --- |
544+| `ubuntu-latest`, or any other Linux label | 2 | 8 GiB | 16 GB | $0.004 |
545+| `g1t-2core` (a name of the standard machine) | 2 | 8 GiB | 16 GB | $0.004 |
546+| `g1t-4core` | 4 | 12 GiB | 20 GB | $0.007 |
548547
549548 ```yaml
550549 jobs:
552551 runs-on: g1t-4core
553552 ```
554553
555−The label can come from the matrix or the run's inputs
556−(`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`). A
557−larger machine costs what it costs g1t, plus the same margin as all
558−sandbox time: see [usage and billing](/guides/usage-and-billing/#workflow-jobs-on-larger-machines).
559−Builds that compile, such as Rust or a large TypeScript project, finish
560−several times faster on one.
554+`g1t-4core` is for heavier jobs: builds that compile, such as Rust or a
555+large TypeScript project, image builds and databases. `g1t-2core` is kept
556+as a name of the standard machine, so workflows that ask for it keep
557+working. The label can come from the matrix or the run's inputs
558+(`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`).
559+
560+Each job is charged its time rounded up to the whole minute (61 seconds
561+is 2 minutes, and a job that ran is at least 1), at its machine's rate:
562+see [runner minutes](/guides/usage-and-billing/#runner-minutes). Jobs on
563+[self-hosted runners](/guides/self-hosted-runners/) are free.
561564
562565 A job on g1t's machines runs for at most 60 minutes, whatever its
563566 `timeout-minutes`, and the workflow's page warns about a job that asks
751754
752755 - **Machine.** Containers share the job's machine: its vCPUs, memory and
753756 disk ([machine sizes](#machine-sizes)). Image builds and databases want
754− `g1t-2core` or `g1t-4core`. `--cpus` and `--memory` limit a container
755− within that.
757+ `g1t-4core`. `--cpus` and `--memory` limit a container within that.
756758 - **Disk.** Images take room on the job's disk. On a machine whose disk
757759 cannot hold layered images, the Engine stores plain copies, which take
758760 more room; the log says when it does.
20022004 runs them too, after a card check, until the month's pool is spent.
20032005
20042006 Before each job starts, g1t reserves what it may cost (its time limit at
2005−the sandbox price) with billing, and settles what it really cost when it
2006−ends; each job's sandbox is charged as
2007−[sandbox time](/guides/usage-and-billing/#sandbox-time), from the first
2008−second. A job billing refuses does not start: it is recorded as failed
2007+its machine's price) with billing, and settles what it really cost when
2008+it ends; each job is charged in
2009+[runner minutes](/guides/usage-and-billing/#runner-minutes), its time
2010+rounded up to the whole minute, at the rate of the machine it ran on. A job billing refuses does not start: it is recorded as failed
20092011 with "Not started:" and the reason, such as "Workflows run in g1t's
20102012 sandboxes, which cost real money, so they need the g1t plan ($20 a month)
20112013 or a card check", and what to do about it. The
+6−5
305305 minutes, so a deploy never cuts one short; an agent run whose guardrails
306306 allow more than two hours can be.
307307
308−Jobs choose larger machines with `runs-on: g1t-2core` or `g1t-4core`.
309−Every sandbox has 20 GB of disk: the standard one and `g1t-2core` are
310−custom instance types (1 vCPU and 4 GiB, and 2 vCPU and 8 GiB), and
311−`g1t-4core` is `standard-4`. Your account's Containers limits must allow
312−all three.
308+The standard sandbox is the instance type `standard-3` (2 vCPU, 8 GiB,
309+16 GB), and so is `runs-on: g1t-2core`, in a class of its own; jobs that
310+ask for `runs-on: g1t-4core` get `standard-4` (4 vCPU, 12 GiB, 20 GB). At
311+the `max_instances` in `services/runner/wrangler.jsonc` (20, 10 and 10),
312+40 sandboxes at once hold 100 vCPU, 360 GiB of memory and 680 GB of disk,
313+which your account's Containers limits for live instances must allow.
313314
314315 Set the runner Worker's `DOCKER` variable to `off` to give workflow jobs no
315316 Docker Engine.
+1−1
158158 [statement](/guides/usage-and-billing/), with each line at its price: what
159159 was charged plus what your plan's included usage, a trial, a pool or a
160160 discount paid of it. The lines are the statement's kinds of charge, such
161−as **Sandbox time**, **Self-hosted runner time** and
161+as **Runner minutes, 2-core**, **Self-hosted runner time** and
162162 **Deployments**, most first. Payments, credits, refunds, tax and card fees
163163 are money in, not spend, and are left out. Under the total are the days
164164 counted and the month so far, at price.
+1−1
273273 2. **With a short probe.** If they share files, a sandbox merges the two
274274 commits and pushes nothing, and reports the files that
275275 conflict. Meanwhile the box says **Checking whether this merges cleanly**,
276− and the merge button waits. Probes are metered as sandbox time; each
276+ and the merge button waits. Probes are charged as runner minutes; each
277277 pair of commits is probed once, and a repository runs at most
278278 three at a time, the rest following in turn.
279279
+1−1
431431
432432 | Part | What it shows |
433433 | --- | --- |
434−| **g1t cloud** | How many workflow jobs are running in g1t's sandboxes now, this month's sandbox time and what it came to at price, and the price per minute from g1t's [price book](/guides/usage-and-billing/). |
434+| **g1t cloud** | How many workflow jobs are running in g1t's sandboxes now, this month's runner minutes and what they came to at price, and each machine's price a minute ([runner minutes](/guides/usage-and-billing/#runner-minutes)). |
435435 | **Your runners** | How many are online, busy, idle and offline, this month's self-hosted time (always $0), how many jobs are waiting for one of them, and whether they take [g1t's work](#g1ts-work-on-your-runners). |
436436 | **On g1t cloud now** | Each running workflow job, linked to its run, with its kind, project and when it started. Work handed to one of your runners is listed under that runner instead. |
437437 | **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). |
+72−48
9898 on $20.
9999
100100 - **$10 of usage each month**, used first: everything g1t runs, at cost
101− plus 20%.
101+ plus 20%. That is 2,500 runner minutes on the standard machine, or
102+ about 1,428 on `g1t-4core`.
102103 - **Everyone in the workspace** at one price, never per person.
103104 - **Unlimited** projects, previews and repositories.
104105 - Workflows, the merge queue,
118119 work.
119120
120121 **Included usage** pays for the month's usage first, at the same prices as
121−everything else: sandbox time, builds, app traffic, custom domains,
122+everything else: runner minutes, builds, app traffic, custom domains,
122123 storage, git operations, search embeddings and security scans. Past it,
123124 usage is charged at the same prices. It starts again on the 1st of each
124125 month (UTC). **Unused included usage does not roll over.** Billing shows
128129
129130 Pricing has two rules:
130131
131−1. **Everything g1t runs at cost plus 20%.** Sandboxes, builds,
132− hosting, storage, git operations, search and security scans: what g1t
133− pays for them, plus 20%.
132+1. **Everything g1t runs at cost plus 20%.** Builds, hosting, storage,
133+ git operations, search and security scans: what g1t pays for them,
134+ plus 20%. [Runner minutes](#runner-minutes) are a fixed rate for each
135+ machine, set the same way: what a minute of it costs g1t with half its
136+ vCPUs busy, plus 20%, rounded up to a tenth of a cent, and never under
137+ its cost with every vCPU busy.
134138 2. **Your own runners are free.** Jobs on your
135139 [own runners](/guides/self-hosted-runners/) cost nothing here.
136140
139143
140144 | What | Unit | Costs g1t | You pay |
141145 | --- | --- | --- | --- |
142−| Sandbox time (workflows, checks, the merge queue) | Second | About $0.001 a minute | About $0.0012 a minute |
143−| [Larger machines](#workflow-jobs-on-larger-machines) for workflow jobs (`g1t-2core`, `g1t-4core`) | Second | About 1.9 and 3.4 times a sandbox second | Cost + 20% |
144−| Deploy builds | Second | About $0.001 a minute | About $0.0012 a minute |
146+| [Runner minutes](#runner-minutes) on the standard machine (2 vCPU, 8 GiB, 16 GB): workflow jobs, checks, the merge queue | Minute, each job rounded up | From $0.0014 a minute idle to $0.0038 with both vCPUs busy | $0.004 a minute |
147+| [Runner minutes](#runner-minutes) on `g1t-4core` (4 vCPU, 12 GiB, 20 GB) | Minute, each job rounded up | From $0.0020 a minute idle to $0.0068 with all four vCPUs busy | $0.007 a minute |
148+| Deploy builds | Second | About $0.0015 a minute | About $0.0018 a minute |
145149 | App requests | Million | $0.30 | $0.36 |
146150 | App CPU time | Million CPU milliseconds | $0.02 | $0.024 |
147151 | Custom domains | Domain-month | $0.10 | $0.12 |
154158 | Security scans | Million CPU ms / million rows | $0.02 / $1.00 | $0.024 / $1.20 |
155159 | Projects, previews, apps and repositories | | Next to nothing | Not charged |
156160
157−Sandbox and build seconds follow what Cloudflare bills g1t, so the
158−figures above move a little; the pricing page always has today's.
161+Build seconds follow what Cloudflare bills g1t, so that figure moves a
162+little; the pricing page always has today's. Runner minutes are fixed
163+rates.
159164 Actions cache storage is charged at R2's published price, and is not on
160165 the pricing page yet.
161166
228233 ## The trial
229234
230235 Once its card is checked, a workspace gets **$5 of trial credit**, once. It
231−pays for usage at cost plus 20%: sandbox time, workflows,
232−the merge queue and semantic search. It never pays for deployments.
236+pays for usage at cost plus 20%: runner minutes for checks, workflows
237+and the merge queue (1,250 on the standard machine), and semantic search. It never pays for deployments.
233238
234239 - Each card gets one trial, whichever workspace it is checked in. Prepaid
235240 cards start none.
246251 ## The open-source pool
247252
248253 g1t sets aside **$25 a month** for public repositories, at most **$2 a
249−month** for any one repository. It pays for **checks, workflows and the
250−merge queue on public repositories**, after a card check. It does not pay
254+month** for any one repository: 6,250 and 500 runner minutes on the
255+standard machine. It pays for **checks, workflows and the merge queue on
256+public repositories**, after a card check. It does not pay
251257 for deployments, storage or embeddings.
252258
253259 When the pool or the repository's share is spent, those runs wait for the
259265
260266 | | Charged |
261267 | --- | --- |
262−| The [merge queue](/guides/merge-queue/) | [Sandbox time](#sandbox-time) |
263−| [Workflow](/guides/actions/) jobs | [Sandbox time](#sandbox-time), more on a [larger machine](#workflow-jobs-on-larger-machines) |
268+| Checks and the [merge queue](/guides/merge-queue/) | [Runner minutes](#runner-minutes) |
269+| [Workflow](/guides/actions/) jobs | [Runner minutes](#runner-minutes), at the rate of the machine each ran on |
264270 | [Actions cache](#actions-cache) | What it holds, on the plan only |
265271 | [Deployments](/guides/deployments/) | Builds, requests, CPU time, custom domains and container time, from the first. Projects and previews are not charged. |
266272 | [Private repository storage](#storage-search-embeddings-and-scans) | Past the free 1 GB, on the plan only |
293299
294300 | Meter | Unit |
295301 | --- | --- |
296−| Sandbox time | Per second: memory, disk and the Durable Object behind each container, plus CPU |
302+| Runner minutes | Per minute, each job rounded up, at a fixed rate for each machine |
297303 | Deploy builds | Per second |
298304 | App requests and CPU | Per million requests, per million CPU milliseconds |
299305 | Custom domains | Per domain-month |
327333 days later, and for something charged once a month (git operations,
328334 storage, the actions cache, custom domains, embeddings, scans) at the
329335 start of the month after that, so no month is charged at two prices.
336+- **A new machine** is priced at what it costs from its first minute, so
337+ g1t never charges less than it pays. When the standard machine became
338+ [2 vCPU, 8 GiB and 16 GB](#runner-minutes), its runner minutes and
339+ deploy builds were priced for it at once.
330340 - **Nothing already charged changes.** Each charge records the price it
331341 was made at.
332342
334344 define exactly what they bill; see
335345 [What g1t can't do yet](/about/limitations/#billing).
336346
337−### Sandbox time
347+### Runner minutes
348+
349+Every job g1t runs on its own machines (a workflow job, a check, a merge
350+queue run) is charged by the minute, at a fixed rate for the machine it
351+ran on:
338352
339−Every sandbox g1t starts runs on Cloudflare Containers, and Cloudflare
340−charges g1t for every second of it. So each one is metered by the second,
341−from start to stop, whatever it was for: checks, the merge queue and
342−workflow jobs.
343−There are no free minutes.
353+| `runs-on` | Machine | vCPUs | Memory | Disk | A minute |
354+| --- | --- | --- | --- | --- | --- |
355+| `ubuntu-latest`, any other Linux label, or `g1t-2core` | Standard (`2-core`) | 2 | 8 GiB | 16 GB | $0.004 |
356+| `g1t-4core` | `4-core` | 4 | 12 GiB | 20 GB | $0.007 |
344357
345−A sandbox second is priced in two parts: memory, disk and the Durable
346−Object behind the container, for every second; and CPU, for the vCPU it
347−used. A sandbox that reports its own CPU use is priced on it. One
348−that does not is priced at the average CPU sandboxes use.
358+Checks and the merge queue run on the standard machine. `g1t-2core` is
359+kept as a name of the standard machine, so workflows that ask for it keep
360+working, at its rate.
349361
350−Each sandbox is one line on the [statement](#the-statement), such as
351−*Checks on acme/api#12: 3m 12s of sandbox time*. Deploy builds are not
352−counted here: [Deployments](/guides/deployments/) charges them by the
353−second, from the first, under **Builds**.
362+**How a job's minutes are counted.** Each job's time, from when its
363+machine starts to when it stops, is rounded up to the whole minute: a
364+job of 61 seconds is 2 minutes, and any job that ran is at least 1. Each
365+job is rounded on its own, so a run of three 20-second jobs is 3
366+minutes. There are no free minutes outside the plan's
367+[included usage](#the-g1t-plan), the [trial](#the-trial) and the
368+[open-source pool](#the-open-source-pool), which pay for minutes at these
369+rates.
354370
355−### Workflow jobs on larger machines
371+**Why these rates.** Cloudflare bills g1t for a machine's memory and disk,
372+and the Durable Object behind it, for every second it runs, busy or idle,
373+and for its CPU as it is used. Each rate is what a minute of the machine
374+costs g1t with half its vCPUs busy, plus 20%, rounded up to a tenth of a
375+cent, and never under what it costs with every vCPU busy:
356376
357−A workflow job whose `runs-on` names a [larger machine](/guides/actions/#machine-sizes)
358−runs on a larger Cloudflare Containers instance, and its seconds cost what
359−that instance costs g1t, plus 20%. The standard machine they are compared
360−with has 1 vCPU, 4 GiB and 20 GB:
377+| Machine | Costs g1t a minute, idle | With half its vCPUs busy | With every vCPU busy | A minute |
378+| --- | --- | --- | --- | --- |
379+| Standard (`2-core`) | $0.0014 | $0.0026 | $0.0038 | $0.004 |
380+| `4-core` | $0.0020 | $0.0044 | $0.0068 | $0.007 |
361381
362−| `runs-on` | Instance | Its memory and disk, against the standard machine's | A second with its vCPUs as busy as an average sandbox's |
363−| --- | --- | --- | --- |
364−| `g1t-2core` | 2 vCPU, 8 GiB, 20 GB | 1.8 times | About 1.9 times a sandbox second |
365−| `g1t-4core` | 4 vCPU, 12 GiB, 20 GB | 2.5 times | About 3.4 times a sandbox second |
382+Should a job ever cost g1t more than its minutes at the rate, it is
383+charged that cost instead. A build that compiles often finishes several
384+times sooner on `g1t-4core`, so it can cost about the same.
366385
367−A job that reports its own CPU is priced on it: its memory and disk for
368−every second, and the vCPU-seconds it used. One that does not is priced
369−at the right-hand column. Its line on the statement names the machine,
370−such as *A workflow job in acme/api on g1t-4core: 6m 40s of sandbox
371−time*. A job usually finishes several times sooner on a larger machine,
372−so it often costs about the same.
386+Each job is one line on the [statement](#the-statement), naming its
387+machine and minutes, such as *A workflow job in acme/api on g1t-4core:
388+6m 40s, 7 minutes on the 4-core machine*. Usage, the statement and the API
389+total them as **Runner minutes, 2-core** and **Runner minutes, 4-core**;
390+months before runner minutes show **Sandbox time**, by the second.
391+Deploy builds are not counted here: [Deployments](/guides/deployments/)
392+charges them by the second, from the first, under **Builds**. Jobs on
393+[your own runners](/guides/self-hosted-runners/) are free: they show as
394+**Self-hosted runner time** at $0.
373395
374396 ## Git operations
375397
867889 **Monthly**), stacked by product, with **Cumulative** to add them up.
868890 Hover or focus a column for each product's part; **Show as a table** has
869891 every number.
870−- **The breakdown**: each product family with its meters (sandbox time;
892+- **The breakdown**: each product family with its meters (runner minutes
893+ for each machine;
871894 builds; git operations and private storage with what is free; and so
872895 on), each with a trend line, how much was used and its charge at price.
873896 Open a meter for its projects.
890913
891914 | Line | What it holds |
892915 | --- | --- |
893− | Sandbox time | Each sandbox's time. |
916+ | Runner minutes, 2-core and Runner minutes, 4-core | Each job on g1t's machines, with the minutes charged on that machine. |
917+ | Sandbox time | Each sandbox's time, in months before runner minutes. |
894918 | Self-hosted runner time | Each job on your own runners, at $0. |
895919 | Deployments | Builds as they finish; each month's requests, CPU time, custom domains and container time when it closes. |
896920 | Private storage | Storage past the free 1 GB, once a month. |
+0−0

Binary or large file; its contents are not shown.

+10−17
183183 </div>
184184 }
185185 >
186− <Figure label="This month" hint="Sandbox time on your usage, at price, from the first of the month.">
186+ <Figure label="This month" hint="Runner minutes on your usage, at price, from the first of the month. Each job's time is rounded up to the minute.">
187187 {cost ? (
188188 <>
189189 {machineTime(cost.cloudSeconds)} · {money(cost.cloudMicros)}
192192 <Unknown what="Billing" />
193193 )}
194194 </Figure>
195− {data.prices ? (
196− <Figure label="Price" hint={`From g1t's price book: ${data.prices.map((price) => price.title).join("; ")}.`}>
197− <span className="flex flex-wrap justify-end gap-x-1">
198− {data.prices.map((price, at) => (
199− <span key={price.meter} className="whitespace-nowrap">
200− {at > 0 && "+ "}
201− {perMinute(price.perMinuteMicros)}/{price.unit}
202− </span>
203− ))}
204− </span>
205− </Figure>
206− ) : (
207− <Figure label="Price">
208− <Unknown what="Billing" />
209− </Figure>
210− )}
195+ <Figure label="Price" hint={`A minute of each machine: ${data.prices.map((price) => price.title).join("; ")}.`}>
196+ <span className="flex flex-wrap justify-end gap-x-2">
197+ {data.prices.map((price) => (
198+ <span key={price.machine} className="whitespace-nowrap">
199+ {price.machine} {perMinute(price.perMinuteMicros)}/min
200+ </span>
201+ ))}
202+ </span>
203+ </Figure>
211204 </PlaceCard>
212205
213206 <PlaceCard
+5−0
200200 month={statement.month}
201201 kind={line.kind}
202202 count={line.count}
203+ minutes={line.minutes ?? null}
203204 chargedMicros={atPrice && line.chargedMicros >= 0 ? (line.priceMicros ?? line.chargedMicros) : line.chargedMicros}
204205 coveredMicros={line.coveredMicros ?? 0}
205206 day={group === "day" ? g.key : null}
229230 month,
230231 kind,
231232 count,
233+ minutes,
232234 chargedMicros,
233235 coveredMicros,
234236 day,
238240 month: string;
239241 kind: string;
240242 count: number;
243+ /** Runner minutes on the line, as charged. */
244+ minutes: number | null;
241245 chargedMicros: number;
242246 coveredMicros: number;
243247 day: string | null;
288292 <ChevronRight size={14} className={`shrink-0 text-faint transition-transform ${open ? "rotate-90" : ""}`} />
289293 <span className="grow truncate font-medium">{kind}</span>
290294 <span className="shrink-0 text-xs text-faint tabular-nums">
295+ {minutes !== null && `${minutes.toLocaleString("en-US")} ${minutes === 1 ? "minute" : "minutes"} · `}
291296 {count.toLocaleString("en-US")} {count === 1 ? "entry" : "entries"}
292297 {coveredMicros > 0 && ` · ${charge(coveredMicros)} paid for`}
293298 </span>
+4−5
5252 waiting: OwnWaiting | null;
5353 /** This month's machine time; null when billing did not answer. */
5454 cost: RunnerCost | null;
55− /** A minute on g1t's cloud, from the price book; null when it has none. */
56− prices: CloudPrice[] | null;
55+ /** Each machine on g1t's cloud and its price a minute, from the machine catalogue. */
56+ prices: CloudPrice[];
5757 /** People's computers with the desktop app, as each app last said; null when the actions service did not answer. */
5858 desktops: DesktopRunner[] | null;
5959 };
7575 export async function loadRunnersPage(workspace: string, actor: User, repositories: string[] = [], now = new Date()): Promise<RunnersPageData> {
7676 const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString().slice(0, 10);
7777 const until = now.toISOString().slice(0, 10);
78− const [base, activity, runs, report, book, desktops] = await Promise.all([
78+ const [base, activity, runs, report, desktops] = await Promise.all([
7979 loadRunners({ workspace }, actor, repositories),
8080 runners
8181 .activity(actor, workspace)
8989 .usageReport(workspace, actor, { from, until })
9090 .then((result) => (result.ok ? result.value : null))
9191 .catch(warn("usage report")),
92− billing.prices().catch(warn("prices")),
9392 runners
9493 .desktops(actor, workspace)
9594 .then((result) => (result.ok ? result.value : null))
10099 cloud: cloudNow(runs, activity, RUN_PAGE, RUNNER_ACTIVITY_LIMIT),
101100 waiting: ownWaiting(activity),
102101 cost: runnerCost(report),
103− prices: cloudPrices(book?.prices ?? null),
102+ prices: cloudPrices(),
104103 desktops,
105104 };
106105 }
+26−11
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import type { AgentRun, Price, RunnerActivity, UsageReport } from "@g1t/contracts";
4+import type { AgentRun, RunnerActivity, UsageReport } from "@g1t/contracts";
55
66 import { cloudNow, cloudPrices, machineTime, ownWaiting, perMinute, runnerCost } from "./runners.ts";
77
7474 free: false,
7575 products: [
7676 { key: "agent", label: "Agent", micros: 0, meters: [meter("agent_sandbox", 600, 1_000_000), meter("agent_models", 9, 5_000_000)] },
77− { key: "sandboxes", label: "Sandboxes", micros: 0, meters: [meter("sandbox", 120, 200_000), meter("self_hosted", 3600, 0)] },
77+ {
78+ key: "sandboxes",
79+ label: "Sandboxes",
80+ micros: 0,
81+ meters: [
82+ meter("runner_2-core", 10, 40_000),
83+ meter("runner_4-core", 2, 14_000),
84+ meter("sandbox", 120, 200_000),
85+ meter("self_hosted", 3600, 0),
86+ ],
87+ },
7888 ],
7989 } as unknown as UsageReport;
80− assert.deepEqual(runnerCost(report), { from: "2026-10-01", until: "2026-10-10", cloudSeconds: 720, cloudMicros: 1_200_000, ownSeconds: 3600, free: false });
90+ // Runner minutes count as their minutes; older sandbox time by the second.
91+ assert.deepEqual(runnerCost(report), {
92+ from: "2026-10-01",
93+ until: "2026-10-10",
94+ cloudSeconds: 720 + 12 * 60,
95+ cloudMicros: 1_254_000,
96+ ownSeconds: 3600,
97+ free: false,
98+ });
8199 });
82100
83−test("prices are the sandbox's parts when both are priced, else the one price, per minute", () => {
84− const price = (meter: string, unit: string, priceMicros: number) => ({ meter, title: meter, unit, priceMicros }) as Price;
101+test("prices are each machine's rate a minute, from the catalogue", () => {
85102 assert.deepEqual(
86− cloudPrices([price("sandbox_second", "second", 25), price("sandbox_base_second", "second", 14.5), price("sandbox_cpu_second", "vCPU-second", 24)])!.map((p) => [p.meter, p.unit, p.perMinuteMicros]),
103+ cloudPrices().map((p) => [p.machine, p.perMinuteMicros, p.title]),
87104 [
88− ["sandbox_base_second", "minute", 870],
89− ["sandbox_cpu_second", "vCPU-minute", 1440],
105+ ["2-core", 4000, "2-core: 2 vCPU, 8 GiB, 16 GB"],
106+ ["4-core", 7000, "4-core: 4 vCPU, 12 GiB, 20 GB"],
90107 ],
91108 );
92− assert.deepEqual(cloudPrices([price("sandbox_second", "second", 25)])!.map((p) => p.perMinuteMicros), [1500]);
93− assert.equal(cloudPrices([]), null);
94− assert.equal(cloudPrices(null), null);
109+ assert.equal(perMinute(4000), "$0.004");
95110 });
96111
97112 test("machine time and prices read as people say them", () => {
+24−26
1010 * (`runner_activity`'s `handed_over`), and its workflow jobs running in
1111 * g1t's sandboxes (`cloud_jobs`).
1212 * - **This month** is billing's usage report from the first of the month:
13− * sandbox time is g1t's cloud, at price (older months may also name
14− * agent sandbox time);
13+ * runner minutes by machine are g1t's cloud, at price (older months
14+ * name sandbox time by the second, and agent sandbox time);
1515 * self-hosted runner time is the workspace's own, at $0.
1616 */
17−import type { AgentRun, MeterLine, Price, RunnerActivity, UsageReport } from "@g1t/contracts";
17+import type { AgentRun, MeterLine, RunnerActivity, UsageReport } from "@g1t/contracts";
18+import { machineSize, machines } from "@g1t/contracts/instances";
1819
1920 /** One thing running in one of g1t's sandboxes. */
2021 export type CloudWork = {
5556 free: boolean;
5657 };
5758
58−/** A price for time on g1t's cloud, per minute, from the price book. */
59−export type CloudPrice = { meter: string; title: string; unit: string; perMinuteMicros: number };
59+/** A machine on g1t's cloud and its price a minute, from the machine catalogue. */
60+export type CloudPrice = { machine: string; title: string; perMinuteMicros: number };
6061
61−/** Billing's meters for machine time (`METERS` in services/billing/src/report.rs); `agent_sandbox` is in older months' reports. */
62−export const CLOUD_METERS = ["agent_sandbox", "sandbox"] as const;
62+/**
63+ * Billing's meters for machine time (`meters()` in services/billing/src/report.rs):
64+ * runner minutes by machine (`runner_2-core`), and by the second, sandbox time
65+ * from before minutes (`agent_sandbox` in still older months' reports).
66+ */
67+export const RUNNER_METER_PREFIX = "runner_";
68+export const CLOUD_SECOND_METERS = ["agent_sandbox", "sandbox"] as const;
6369 export const OWN_METER = "self_hosted";
64−
65−/** The price book's meters for a sandbox's time: its parts when both are priced, else the one price. */
66−const PRICE_PARTS = ["sandbox_base_second", "sandbox_cpu_second"] as const;
67−const PRICE_WHOLE = "sandbox_second";
6870
6971 /** Where a sandbox run (checks, the merge queue) shows: its pull request, else the repository's pull requests. */
7072 export function sandboxRunHref(repo: string, number?: number | null): string {
126128 const meters: MeterLine[] = report.products.flatMap((product) => product.meters);
127129 const sum = (keys: readonly string[], pick: (meter: MeterLine) => number) =>
128130 meters.filter((meter) => keys.includes(meter.key)).reduce((total, meter) => total + pick(meter), 0);
131+ const runnerKeys = meters.filter((meter) => meter.key.startsWith(RUNNER_METER_PREFIX)).map((meter) => meter.key);
129132 return {
130133 from: report.from,
131134 until: report.until,
132− cloudSeconds: sum(CLOUD_METERS, (meter) => meter.quantity),
133− cloudMicros: sum(CLOUD_METERS, (meter) => meter.micros + (meter.pendingMicros ?? 0)),
135+ // Minutes as charged, and sandbox time from before them.
136+ cloudSeconds: sum(runnerKeys, (meter) => meter.quantity * 60) + sum(CLOUD_SECOND_METERS, (meter) => meter.quantity),
137+ cloudMicros: sum([...runnerKeys, ...CLOUD_SECOND_METERS], (meter) => meter.micros + (meter.pendingMicros ?? 0)),
134138 ownSeconds: sum([OWN_METER], (meter) => meter.quantity),
135139 free: report.free,
136140 };
137141 }
138142
139−/** The price of a minute on g1t's cloud, as the price book has it; null when it has none. */
140−export function cloudPrices(prices: Price[] | null): CloudPrice[] | null {
141− if (!prices) return null;
142− const perMinute = (price: Price): CloudPrice => ({
143− meter: price.meter,
144− title: price.title,
145− unit: price.unit.replace(/second$/, "minute"),
146− perMinuteMicros: price.priceMicros * 60,
147− });
148− const parts = PRICE_PARTS.map((meter) => prices.find((price) => price.meter === meter));
149− if (parts.every(Boolean)) return parts.map((price) => perMinute(price!));
150− const whole = prices.find((price) => price.meter === PRICE_WHOLE);
151− return whole ? [perMinute(whole)] : null;
143+/** Each machine on g1t's cloud and its price a minute, from the machine catalogue. */
144+export function cloudPrices(): CloudPrice[] {
145+ return machines().map((instance) => ({
146+ machine: instance.machine,
147+ title: `${instance.machine}: ${machineSize(instance)}`,
148+ perMinuteMicros: instance.microsPerMinute,
149+ }));
152150 }
153151
154152 /** Seconds as people read machine time: "45s", "12 min", "3.4 h". */
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…