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

Hosted runner time is charged by the job, in minutes: each job's time on g1t's machines (a workflow job, a check, a merge queue run) is rounded up to the whole minute, so 61 seconds is 2 minutes and any job that ran is at least 1, and charged at a fixed rate for the machine it ran on: $0.004 a minute for the standard machine (2 vCPU, 8 GiB, 16 GB; ubuntu-latest, every other hosted label and g1t-2core) and $0.007 for g1t-4core (4 vCPU, 12 GiB, 20 GB). Each rate is the machine's cost a minute at Cloudflare's list prices with half its vCPUs busy (memory, disk and the Durable Object for every second, CPU as used), plus the 20% margin, rounded up to a tenth of a cent, and never under its cost with every vCPU busy ($0.0038 and $0.0068); should a job ever cost g1t more than its minutes at the rate, it is charged that cost. Jobs on self-hosted runners stay free.

The machine catalogue is the single source for this. Each row of INSTANCE_TYPES in the contracts, mirrored row for row in packages/contracts/src/instances.ts, is a runs-on label, the machine it bills as (2-core, 4-core), the runner's Durable Object class that runs it, the Cloudflare instance type behind that class, its size and its rate a minute; price_scale is now derived from the rates. The runner starts a job in the class its label's row names (jobNamespaceOf, by SANDBOX_BINDINGS) instead of checking labels by name, and refuses one whose class is not deployed rather than running it elsewhere. Tests hold the rows to their machines, the rates to the list prices, the TypeScript mirror to the Rust table, and every class in wrangler.jsonc, its instance type, size and binding to the catalogue. CONTRIBUTING.md has "Adding a machine size". Billing records each job's machine on its ledger line (migration 0057 adds ledger.machine) and its minutes as the line's quantity; what it cost g1t is recorded as before, now at the machine's vCPUs as busy as the standard one's when its CPU is unknown. Lines from before minutes have no machine and keep reading as Sandbox time, by the second. The Usage page and the API's usage report have a Runner minutes meter for each machine in the catalogue, in minutes; the statement and invoices have a Runner minutes line for each machine, and statement lines carry the minutes; Billing's meter is Runner minutes & other usage, with this month's minutes by machine. The Runners page counts runner minutes as cloud time and shows each machine's rate a minute from the catalogue, and the pricing page lists runner minutes for each machine, with what it costs g1t at full CPU, in place of the sandbox second meters. The plan's included usage, the trial and the open-source pool pay for minutes at these rates, and the docs say how many minutes each comes to. The Actions and usage-and-billing guides have the machine sizes with their price a minute, how a job's minutes are counted, why the rates are what they are, and that self-hosted runners are free; the OpenAPI document's usage example shows a runner-minutes meter.

syntaqxcommitted Parente8ac472Browse files
30 files+245−14414 files not counted0/30 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
63676367 "from": "2026-10-01",
63686368 "until": "2026-10-07",
63696369 "totals": {
6370− "price_micros": 48210000,
6370+ "price_micros": 48212000,
63716371 "discount_micros": 0,
63726372 "included_micros": 20000000,
63736373 "credits_micros": 18000000,
6374− "charged_micros": 10210000,
6374+ "charged_micros": 10212000,
63756375 "pending_micros": 1340000,
63766376 "pending_charged_micros": 1340000,
63776377 "cost_micros": 40100000
63976397 {
63986398 "key": "sandboxes",
63996399 "label": "Sandboxes",
6400− "micros": 7210000,
6400+ "micros": 7212000,
64016401 "meters": [
64026402 {
6403− "key": "sandbox",
6404− "label": "Sandbox time",
6403+ "key": "runner_2-core",
6404+ "label": "Runner minutes, 2-core (2 vCPU, 8 GiB, 16 GB)",
64056405 "product": "sandboxes",
6406− "unit": "seconds",
6407− "quantity": 41300,
6408− "micros": 7210000,
6406+ "unit": "minutes",
6407+ "quantity": 1803,
6408+ "micros": 7212000,
64096409 "pending_micros": 0,
64106410 "daily": [
64116411 900000,
64146414 1100000,
64156415 0,
64166416 2100000,
6417− 1110000
6417+ 1112000
64186418 ],
6419− "allowance": {
6420− "used": 41300,
6421− "of": 108000,
6422− "unit": "seconds"
6423− },
6419+ "allowance": null,
64246420 "by_project": [
64256421 {
64266422 "project": "flagon-io/g1t",
6427− "micros": 7210000,
6428− "quantity": 41300
6423+ "micros": 7212000,
6424+ "quantity": 1803
64296425 }
64306426 ]
64316427 }
+15−11
539539
540540 A job runs on the standard machine unless its `runs-on` is `g1t-4core`:
541541
542−| `runs-on` | vCPUs | Memory | Disk |
543−| --- | --- | --- | --- |
544−| `ubuntu-latest`, or any other Linux label | 2 | 8 GiB | 16 GB |
545−| `g1t-2core` (a name of the standard machine) | 2 | 8 GiB | 16 GB |
546−| `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 |
547547
548548 ```yaml
549549 jobs:
555555 large TypeScript project, image builds and databases. `g1t-2core` is kept
556556 as a name of the standard machine, so workflows that ask for it keep
557557 working. The label can come from the matrix or the run's inputs
558−(`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`). What
559−each costs is in [usage and billing](/guides/usage-and-billing/#machines).
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.
560564
561565 A job on g1t's machines runs for at most 60 minutes, whatever its
562566 `timeout-minutes`, and the workflow's page warns about a job that asks
19992003 runs them too, after a card check, until the month's pool is spent.
20002004
20012005 Before each job starts, g1t reserves what it may cost (its time limit at
2002−the sandbox price) with billing, and settles what it really cost when it
2003−ends; each job's sandbox is charged as
2004−[sandbox time](/guides/usage-and-billing/#sandbox-time), from the first
2005−second. A job billing refuses does not start: it is recorded as failed
2006+its machine's price) with billing, and settles what it really cost when
2007+it ends; each job is charged in
2008+[runner minutes](/guides/usage-and-billing/#runner-minutes), its time
2009+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
20062010 with "Not started:" and the reason, such as "Workflows run in g1t's
20072011 sandboxes, which cost real money, so they need the g1t plan ($20 a month)
20082012 or a card check", and what to do about it. The
+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/). |
+70−54
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) on [the standard machine](#machines) (2 vCPU, 8 GiB, 16 GB) | Second | About $0.0015 a minute: from $0.0014 idle to $0.0038 with both vCPUs busy | About $0.0018 a minute: from $0.0016 to $0.0045 |
143−| Workflow jobs on [`g1t-4core`](#machines) (4 vCPU, 12 GiB, 20 GB) | Second | About 1.8 times a sandbox second: from $0.0020 a minute idle to $0.0068 with all four vCPUs busy | Cost + 20%: from $0.0024 to $0.0081 |
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 |
144148 | 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 |
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 [`g1t-4core`](#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.
330−- **A new machine** is priced at what it costs from its first second, so
331− g1t never charges less than it pays. When the standard sandbox became
332− [2 vCPU, 8 GiB and 16 GB](#machines), sandbox time and deploy builds
333− rose with it at once.
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.
334340 - **Nothing already charged changes.** Each charge records the price it
335341 was made at.
336342
338344 define exactly what they bill; see
339345 [What g1t can't do yet](/about/limitations/#billing).
340346
341−### Sandbox time
347+### Runner minutes
342348
343−Every sandbox g1t starts runs on Cloudflare Containers, and Cloudflare
344−charges g1t for every second of it. So each one is metered by the second,
345−from start to stop, whatever it was for: checks, the merge queue and
346−workflow jobs.
347−There are no free minutes.
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:
348352
349−A sandbox second is priced in two parts: memory, disk and the Durable
350−Object behind the container, for every second; and CPU, for the vCPU it
351−used. A sandbox that reports its own CPU use is priced on it. One
352−that does not is priced at the average CPU sandboxes use.
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 |
353357
354−Each sandbox is one line on the [statement](#the-statement), such as
355−*Checks on acme/api#12: 3m 12s of sandbox time*. Deploy builds are not
356−counted here: [Deployments](/guides/deployments/) charges them by the
357−second, from the first, under **Builds**.
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.
358361
359−### Machines
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.
360370
361−Checks, the merge queue, deploy builds and workflow jobs run on the
362−standard machine: 2 vCPU, 8 GiB of memory and 16 GB of disk. A workflow
363−job whose `runs-on` is `g1t-4core` gets 4 vCPU, 12 GiB and 20 GB, for
364−heavier jobs. `g1t-2core` is kept as a name of the standard machine, so
365−workflows that ask for it keep working, at the standard price.
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:
366376
367−| `runs-on` | Machine | Memory, disk and the Durable Object, billed every second | CPU, billed as used | A second without its own CPU reported |
377+| Machine | Costs g1t a minute, idle | With half its vCPUs busy | With every vCPU busy | A minute |
368378 | --- | --- | --- | --- | --- |
369−| Any hosted label, and `g1t-2core` | 2 vCPU, 8 GiB, 16 GB | About $0.0014 a minute | $0.00002 a vCPU-second, up to $0.0024 a minute | A sandbox second |
370−| `g1t-4core` | 4 vCPU, 12 GiB, 20 GB | About $0.0020 a minute, 1.45 times the standard machine's | Up to $0.0048 a minute | About 1.8 times a sandbox second |
379+| Standard (`2-core`) | $0.0014 | $0.0026 | $0.0038 | $0.004 |
380+| `4-core` | $0.0020 | $0.0044 | $0.0068 | $0.007 |
371381
372−Those are what each costs g1t; you pay them plus 20%. A job that reports
373−its own CPU is priced on it: its memory, disk and Durable Object for
374−every second, and the vCPU-seconds it used. One that does not is priced
375−at the right-hand column, from the average CPU sandboxes use. Either way,
376−an idle second is charged at no less than its memory, disk and Durable
377−Object cost g1t, plus 20%. A job's line on the statement names the
378−machine, such as *A workflow job in acme/api on g1t-4core: 6m 40s of
379−sandbox time*. A build that compiles often finishes sooner on
380−`g1t-4core`, so it can cost about the same.
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.
381385
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.
395+
382396 ## Git operations
383397
384398 Cloudflare charges g1t **$0.15 per 1,000 git operations** from 2026-10-14.
875889 **Monthly**), stacked by product, with **Cumulative** to add them up.
876890 Hover or focus a column for each product's part; **Show as a table** has
877891 every number.
878−- **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;
879894 builds; git operations and private storage with what is free; and so
880895 on), each with a trend line, how much was used and its charge at price.
881896 Open a meter for its projects.
898913
899914 | Line | What it holds |
900915 | --- | --- |
901− | 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. |
902918 | Self-hosted runner time | Each job on your own runners, at $0. |
903919 | Deployments | Builds as they finish; each month's requests, CPU time, custom domains and container time when it closes. |
904920 | 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". */
+2−0
5858 test("quantities read in their units", () => {
5959 assert.equal(quantity(1_234_567, "tokens"), "1.2M tokens");
6060 assert.equal(quantity(3_725, "seconds"), "1h 2m");
61+ assert.equal(quantity(1, "minutes"), "1 minute");
62+ assert.equal(quantity(1_240, "minutes"), "1,240 minutes");
6163 assert.equal(quantity(504_000_000, "bytes"), "504 MB");
6264 assert.equal(quantity(1, "entries"), "1 entry");
6365 });
+2−0
192192 const m = Math.floor((s % 3600) / 60);
193193 return h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
194194 }
195+ case "minutes":
196+ return `${compact(amount)} ${Math.round(amount) === 1 ? "minute" : "minutes"}`;
195197 case "bytes":
196198 return bytes(amount);
197199 case "operations":
+31−1
11 import { ArrowUpRight } from "lucide-react";
22
33 import type { FeaturePlan, FreeTier, Price } from "@g1t/contracts";
4+import { type InstanceType, machineSize, machines } from "@g1t/contracts/instances";
45
56 import type { Route } from "./+types/pricing";
67 import { money as sum, wholeDollars } from "../lib/money";
6465 "card_fee_percent",
6566 "card_fee_fixed",
6667 "security_activation",
68+ // What a sandbox second costs g1t: runner minutes are charged at each
69+ // machine's rate, in rows of their own.
70+ "sandbox_second",
71+ "sandbox_base_second",
72+ "sandbox_cpu_second",
6773 ]);
6874
75+/**
76+ * What a minute of a machine costs g1t at Cloudflare's list prices with
77+ * every vCPU busy: memory, disk and the Durable Object behind it (128 MB)
78+ * for every second, and its vCPUs.
79+ */
80+function busyMinuteMicros(instance: InstanceType): number {
81+ return 60 * (instance.memoryGib * 2.5 + instance.diskGb * 0.07 + 0.125 * 12.5 + instance.vcpu * 20);
82+}
83+
6984 /** A retired meter: agents' models and tokens, and the AI Gateway. */
7085 function retired(meter: string): boolean {
7186 return meter.startsWith("agent_") || meter === "gateway_models";
148163 const HOW = [
149164 {
150165 title: "Our cost, passed through",
151− body: "Every sandbox second, build and app request costs g1t money at Cloudflare. Each is metered and charged at that cost plus 20%, from the first second and the first request. Use a little, pay a little.",
166+ body: "Every runner minute, build and app request costs g1t money at Cloudflare. Builds and app traffic are charged at that cost plus 20%, from the first second and the first request; runner minutes at a fixed rate for each machine, set from its cost plus 20% and never under its cost with every vCPU busy. Use a little, pay a little.",
152167 },
153168 {
154169 title: "Prices follow costs, by themselves",
612627 </>
613628 );
614629 })()}
630+ {machines().map((instance) => (
631+ <tr key={instance.machine}>
632+ <td className="px-4 py-3">
633+ <p className="font-medium">
634+ Runner minutes, {instance.label === "standard" ? "standard machine" : instance.label}
635+ </p>
636+ <p className="text-xs text-faint">
637+ {machineSize(instance)}. Each job's time rounded up to the minute; checks, workflows and the merge queue
638+ </p>
639+ </td>
640+ <td className="px-4 py-3 font-mono text-xs tabular-nums text-muted">Up to {money(busyMinuteMicros(instance))} a minute</td>
641+ <td className="hidden px-4 py-3 tabular-nums sm:table-cell">Fixed rate</td>
642+ <td className="px-4 py-3 font-mono text-xs tabular-nums">{money(instance.microsPerMinute)} a minute</td>
643+ </tr>
644+ ))}
615645 <tr>
616646 <td className="px-4 py-3">
617647 <p className="font-medium">Your own runners</p>
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…

Comments

Sign in to comment.