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.
| 220 | 220 | `g1t cache usage` says what the repository's entries hold against its | |
| 221 | 221 | 10 GiB, and `g1t cache list --sort size-in-bytes` which take the most. | |
| 222 | 222 | ||
| 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 | + | ||
| 223 | 264 | ## Speed | |
| 224 | 265 | ||
| 225 | 266 | Pages are a few rounds of service calls, and each round costs a trip to |
| 6367 | 6367 | "from": "2026-10-01", | |
| 6368 | 6368 | "until": "2026-10-07", | |
| 6369 | 6369 | "totals": { | |
| 6370 | − | "price_micros": 48210000, | |
| 6370 | + | "price_micros": 48212000, | |
| 6371 | 6371 | "discount_micros": 0, | |
| 6372 | 6372 | "included_micros": 20000000, | |
| 6373 | 6373 | "credits_micros": 18000000, | |
| 6374 | − | "charged_micros": 10210000, | |
| 6374 | + | "charged_micros": 10212000, | |
| 6375 | 6375 | "pending_micros": 1340000, | |
| 6376 | 6376 | "pending_charged_micros": 1340000, | |
| 6377 | 6377 | "cost_micros": 40100000 | |
| ⋯ | |||
| 6397 | 6397 | { | |
| 6398 | 6398 | "key": "sandboxes", | |
| 6399 | 6399 | "label": "Sandboxes", | |
| 6400 | − | "micros": 7210000, | |
| 6400 | + | "micros": 7212000, | |
| 6401 | 6401 | "meters": [ | |
| 6402 | 6402 | { | |
| 6403 | − | "key": "sandbox", | |
| 6404 | − | "label": "Sandbox time", | |
| 6403 | + | "key": "runner_2-core", | |
| 6404 | + | "label": "Runner minutes, 2-core (2 vCPU, 8 GiB, 16 GB)", | |
| 6405 | 6405 | "product": "sandboxes", | |
| 6406 | − | "unit": "seconds", | |
| 6407 | − | "quantity": 41300, | |
| 6408 | − | "micros": 7210000, | |
| 6406 | + | "unit": "minutes", | |
| 6407 | + | "quantity": 1803, | |
| 6408 | + | "micros": 7212000, | |
| 6409 | 6409 | "pending_micros": 0, | |
| 6410 | 6410 | "daily": [ | |
| 6411 | 6411 | 900000, | |
| ⋯ | |||
| 6414 | 6414 | 1100000, | |
| 6415 | 6415 | 0, | |
| 6416 | 6416 | 2100000, | |
| 6417 | − | 1110000 | |
| 6417 | + | 1112000 | |
| 6418 | 6418 | ], | |
| 6419 | − | "allowance": { | |
| 6420 | − | "used": 41300, | |
| 6421 | − | "of": 108000, | |
| 6422 | − | "unit": "seconds" | |
| 6423 | − | }, | |
| 6419 | + | "allowance": null, | |
| 6424 | 6420 | "by_project": [ | |
| 6425 | 6421 | { | |
| 6426 | 6422 | "project": "flagon-io/g1t", | |
| 6427 | − | "micros": 7210000, | |
| 6428 | − | "quantity": 41300 | |
| 6423 | + | "micros": 7212000, | |
| 6424 | + | "quantity": 1803 | |
| 6429 | 6425 | } | |
| 6430 | 6426 | ] | |
| 6431 | 6427 | } | |
| 539 | 539 | ||
| 540 | 540 | A job runs on the standard machine unless its `runs-on` is `g1t-4core`: | |
| 541 | 541 | ||
| 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 | | |
| 547 | 547 | ||
| 548 | 548 | ```yaml | |
| 549 | 549 | jobs: | |
| ⋯ | |||
| 555 | 555 | large TypeScript project, image builds and databases. `g1t-2core` is kept | |
| 556 | 556 | as a name of the standard machine, so workflows that ask for it keep | |
| 557 | 557 | 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. | |
| 560 | 564 | ||
| 561 | 565 | A job on g1t's machines runs for at most 60 minutes, whatever its | |
| 562 | 566 | `timeout-minutes`, and the workflow's page warns about a job that asks | |
| ⋯ | |||
| 1999 | 2003 | runs them too, after a card check, until the month's pool is spent. | |
| 2000 | 2004 | ||
| 2001 | 2005 | 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 | |
| 2006 | 2010 | with "Not started:" and the reason, such as "Workflows run in g1t's | |
| 2007 | 2011 | sandboxes, which cost real money, so they need the g1t plan ($20 a month) | |
| 2008 | 2012 | or a card check", and what to do about it. The | |
| 158 | 158 | [statement](/guides/usage-and-billing/), with each line at its price: what | |
| 159 | 159 | was charged plus what your plan's included usage, a trial, a pool or a | |
| 160 | 160 | 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 | |
| 162 | 162 | **Deployments**, most first. Payments, credits, refunds, tax and card fees | |
| 163 | 163 | are money in, not spend, and are left out. Under the total are the days | |
| 164 | 164 | counted and the month so far, at price. |
| 273 | 273 | 2. **With a short probe.** If they share files, a sandbox merges the two | |
| 274 | 274 | commits and pushes nothing, and reports the files that | |
| 275 | 275 | 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 | |
| 277 | 277 | pair of commits is probed once, and a repository runs at most | |
| 278 | 278 | three at a time, the rest following in turn. | |
| 279 | 279 |
| 431 | 431 | ||
| 432 | 432 | | Part | What it shows | | |
| 433 | 433 | | --- | --- | | |
| 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)). | | |
| 435 | 435 | | **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). | | |
| 436 | 436 | | **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. | | |
| 437 | 437 | | **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). | |
| 98 | 98 | on $20. | |
| 99 | 99 | ||
| 100 | 100 | - **$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`. | |
| 102 | 103 | - **Everyone in the workspace** at one price, never per person. | |
| 103 | 104 | - **Unlimited** projects, previews and repositories. | |
| 104 | 105 | - Workflows, the merge queue, | |
| ⋯ | |||
| 118 | 119 | work. | |
| 119 | 120 | ||
| 120 | 121 | **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, | |
| 122 | 123 | storage, git operations, search embeddings and security scans. Past it, | |
| 123 | 124 | usage is charged at the same prices. It starts again on the 1st of each | |
| 124 | 125 | month (UTC). **Unused included usage does not roll over.** Billing shows | |
| ⋯ | |||
| 128 | 129 | ||
| 129 | 130 | Pricing has two rules: | |
| 130 | 131 | ||
| 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. | |
| 134 | 138 | 2. **Your own runners are free.** Jobs on your | |
| 135 | 139 | [own runners](/guides/self-hosted-runners/) cost nothing here. | |
| 136 | 140 | ||
| ⋯ | |||
| 139 | 143 | ||
| 140 | 144 | | What | Unit | Costs g1t | You pay | | |
| 141 | 145 | | --- | --- | --- | --- | | |
| 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 | | |
| 144 | 148 | | Deploy builds | Second | About $0.0015 a minute | About $0.0018 a minute | | |
| 145 | 149 | | App requests | Million | $0.30 | $0.36 | | |
| 146 | 150 | | App CPU time | Million CPU milliseconds | $0.02 | $0.024 | | |
| ⋯ | |||
| 154 | 158 | | Security scans | Million CPU ms / million rows | $0.02 / $1.00 | $0.024 / $1.20 | | |
| 155 | 159 | | Projects, previews, apps and repositories | | Next to nothing | Not charged | | |
| 156 | 160 | ||
| 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. | |
| 159 | 164 | Actions cache storage is charged at R2's published price, and is not on | |
| 160 | 165 | the pricing page yet. | |
| 161 | 166 | ||
| ⋯ | |||
| 228 | 233 | ## The trial | |
| 229 | 234 | ||
| 230 | 235 | 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. | |
| 233 | 238 | ||
| 234 | 239 | - Each card gets one trial, whichever workspace it is checked in. Prepaid | |
| 235 | 240 | cards start none. | |
| ⋯ | |||
| 246 | 251 | ## The open-source pool | |
| 247 | 252 | ||
| 248 | 253 | 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 | |
| 251 | 257 | for deployments, storage or embeddings. | |
| 252 | 258 | ||
| 253 | 259 | When the pool or the repository's share is spent, those runs wait for the | |
| ⋯ | |||
| 259 | 265 | ||
| 260 | 266 | | | Charged | | |
| 261 | 267 | | --- | --- | | |
| 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 | | |
| 264 | 270 | | [Actions cache](#actions-cache) | What it holds, on the plan only | | |
| 265 | 271 | | [Deployments](/guides/deployments/) | Builds, requests, CPU time, custom domains and container time, from the first. Projects and previews are not charged. | | |
| 266 | 272 | | [Private repository storage](#storage-search-embeddings-and-scans) | Past the free 1 GB, on the plan only | | |
| ⋯ | |||
| 293 | 299 | ||
| 294 | 300 | | Meter | Unit | | |
| 295 | 301 | | --- | --- | | |
| 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 | | |
| 297 | 303 | | Deploy builds | Per second | | |
| 298 | 304 | | App requests and CPU | Per million requests, per million CPU milliseconds | | |
| 299 | 305 | | Custom domains | Per domain-month | | |
| ⋯ | |||
| 327 | 333 | days later, and for something charged once a month (git operations, | |
| 328 | 334 | storage, the actions cache, custom domains, embeddings, scans) at the | |
| 329 | 335 | 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. | |
| 334 | 340 | - **Nothing already charged changes.** Each charge records the price it | |
| 335 | 341 | was made at. | |
| 336 | 342 | ||
| ⋯ | |||
| 338 | 344 | define exactly what they bill; see | |
| 339 | 345 | [What g1t can't do yet](/about/limitations/#billing). | |
| 340 | 346 | ||
| 341 | − | ### Sandbox time | |
| 347 | + | ### Runner minutes | |
| 342 | 348 | ||
| 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: | |
| 348 | 352 | ||
| 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 | | |
| 353 | 357 | ||
| 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. | |
| 358 | 361 | ||
| 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. | |
| 360 | 370 | ||
| 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: | |
| 366 | 376 | ||
| 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 | | |
| 368 | 378 | | --- | --- | --- | --- | --- | | |
| 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 | | |
| 371 | 381 | ||
| 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. | |
| 381 | 385 | ||
| 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 | + | ||
| 382 | 396 | ## Git operations | |
| 383 | 397 | ||
| 384 | 398 | Cloudflare charges g1t **$0.15 per 1,000 git operations** from 2026-10-14. | |
| ⋯ | |||
| 875 | 889 | **Monthly**), stacked by product, with **Cumulative** to add them up. | |
| 876 | 890 | Hover or focus a column for each product's part; **Show as a table** has | |
| 877 | 891 | 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; | |
| 879 | 894 | builds; git operations and private storage with what is free; and so | |
| 880 | 895 | on), each with a trend line, how much was used and its charge at price. | |
| 881 | 896 | Open a meter for its projects. | |
| ⋯ | |||
| 898 | 913 | ||
| 899 | 914 | | Line | What it holds | | |
| 900 | 915 | | --- | --- | | |
| 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. | | |
| 902 | 918 | | Self-hosted runner time | Each job on your own runners, at $0. | | |
| 903 | 919 | | Deployments | Builds as they finish; each month's requests, CPU time, custom domains and container time when it closes. | | |
| 904 | 920 | | Private storage | Storage past the free 1 GB, once a month. | | |
Binary or large file; its contents are not shown.
| 183 | 183 | </div> | |
| 184 | 184 | } | |
| 185 | 185 | > | |
| 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."> | |
| 187 | 187 | {cost ? ( | |
| 188 | 188 | <> | |
| 189 | 189 | {machineTime(cost.cloudSeconds)} · {money(cost.cloudMicros)} | |
| ⋯ | |||
| 192 | 192 | <Unknown what="Billing" /> | |
| 193 | 193 | )} | |
| 194 | 194 | </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> | |
| 211 | 204 | </PlaceCard> | |
| 212 | 205 | ||
| 213 | 206 | <PlaceCard | |
| 200 | 200 | month={statement.month} | |
| 201 | 201 | kind={line.kind} | |
| 202 | 202 | count={line.count} | |
| 203 | + | minutes={line.minutes ?? null} | |
| 203 | 204 | chargedMicros={atPrice && line.chargedMicros >= 0 ? (line.priceMicros ?? line.chargedMicros) : line.chargedMicros} | |
| 204 | 205 | coveredMicros={line.coveredMicros ?? 0} | |
| 205 | 206 | day={group === "day" ? g.key : null} | |
| ⋯ | |||
| 229 | 230 | month, | |
| 230 | 231 | kind, | |
| 231 | 232 | count, | |
| 233 | + | minutes, | |
| 232 | 234 | chargedMicros, | |
| 233 | 235 | coveredMicros, | |
| 234 | 236 | day, | |
| ⋯ | |||
| 238 | 240 | month: string; | |
| 239 | 241 | kind: string; | |
| 240 | 242 | count: number; | |
| 243 | + | /** Runner minutes on the line, as charged. */ | |
| 244 | + | minutes: number | null; | |
| 241 | 245 | chargedMicros: number; | |
| 242 | 246 | coveredMicros: number; | |
| 243 | 247 | day: string | null; | |
| ⋯ | |||
| 288 | 292 | <ChevronRight size={14} className={`shrink-0 text-faint transition-transform ${open ? "rotate-90" : ""}`} /> | |
| 289 | 293 | <span className="grow truncate font-medium">{kind}</span> | |
| 290 | 294 | <span className="shrink-0 text-xs text-faint tabular-nums"> | |
| 295 | + | {minutes !== null && `${minutes.toLocaleString("en-US")} ${minutes === 1 ? "minute" : "minutes"} · `} | |
| 291 | 296 | {count.toLocaleString("en-US")} {count === 1 ? "entry" : "entries"} | |
| 292 | 297 | {coveredMicros > 0 && ` · ${charge(coveredMicros)} paid for`} | |
| 293 | 298 | </span> | |
| 52 | 52 | waiting: OwnWaiting | null; | |
| 53 | 53 | /** This month's machine time; null when billing did not answer. */ | |
| 54 | 54 | 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[]; | |
| 57 | 57 | /** People's computers with the desktop app, as each app last said; null when the actions service did not answer. */ | |
| 58 | 58 | desktops: DesktopRunner[] | null; | |
| 59 | 59 | }; | |
| ⋯ | |||
| 75 | 75 | export async function loadRunnersPage(workspace: string, actor: User, repositories: string[] = [], now = new Date()): Promise<RunnersPageData> { | |
| 76 | 76 | const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString().slice(0, 10); | |
| 77 | 77 | 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([ | |
| 79 | 79 | loadRunners({ workspace }, actor, repositories), | |
| 80 | 80 | runners | |
| 81 | 81 | .activity(actor, workspace) | |
| ⋯ | |||
| 89 | 89 | .usageReport(workspace, actor, { from, until }) | |
| 90 | 90 | .then((result) => (result.ok ? result.value : null)) | |
| 91 | 91 | .catch(warn("usage report")), | |
| 92 | − | billing.prices().catch(warn("prices")), | |
| 93 | 92 | runners | |
| 94 | 93 | .desktops(actor, workspace) | |
| 95 | 94 | .then((result) => (result.ok ? result.value : null)) | |
| ⋯ | |||
| 100 | 99 | cloud: cloudNow(runs, activity, RUN_PAGE, RUNNER_ACTIVITY_LIMIT), | |
| 101 | 100 | waiting: ownWaiting(activity), | |
| 102 | 101 | cost: runnerCost(report), | |
| 103 | − | prices: cloudPrices(book?.prices ?? null), | |
| 102 | + | prices: cloudPrices(), | |
| 104 | 103 | desktops, | |
| 105 | 104 | }; | |
| 106 | 105 | } | |
| 1 | 1 | import assert from "node:assert/strict"; | |
| 2 | 2 | import { test } from "node:test"; | |
| 3 | 3 | ||
| 4 | − | import type { AgentRun, Price, RunnerActivity, UsageReport } from "@g1t/contracts"; | |
| 4 | + | import type { AgentRun, RunnerActivity, UsageReport } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import { cloudNow, cloudPrices, machineTime, ownWaiting, perMinute, runnerCost } from "./runners.ts"; | |
| 7 | 7 | ||
| ⋯ | |||
| 74 | 74 | free: false, | |
| 75 | 75 | products: [ | |
| 76 | 76 | { 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 | + | }, | |
| 78 | 88 | ], | |
| 79 | 89 | } 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 | + | }); | |
| 81 | 99 | }); | |
| 82 | 100 | ||
| 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", () => { | |
| 85 | 102 | 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]), | |
| 87 | 104 | [ | |
| 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"], | |
| 90 | 107 | ], | |
| 91 | 108 | ); | |
| 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"); | |
| 95 | 110 | }); | |
| 96 | 111 | ||
| 97 | 112 | test("machine time and prices read as people say them", () => { | |
| 10 | 10 | * (`runner_activity`'s `handed_over`), and its workflow jobs running in | |
| 11 | 11 | * g1t's sandboxes (`cloud_jobs`). | |
| 12 | 12 | * - **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); | |
| 15 | 15 | * self-hosted runner time is the workspace's own, at $0. | |
| 16 | 16 | */ | |
| 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"; | |
| 18 | 19 | ||
| 19 | 20 | /** One thing running in one of g1t's sandboxes. */ | |
| 20 | 21 | export type CloudWork = { | |
| ⋯ | |||
| 55 | 56 | free: boolean; | |
| 56 | 57 | }; | |
| 57 | 58 | ||
| 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 }; | |
| 60 | 61 | ||
| 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; | |
| 63 | 69 | 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"; | |
| 68 | 70 | ||
| 69 | 71 | /** Where a sandbox run (checks, the merge queue) shows: its pull request, else the repository's pull requests. */ | |
| 70 | 72 | export function sandboxRunHref(repo: string, number?: number | null): string { | |
| ⋯ | |||
| 126 | 128 | const meters: MeterLine[] = report.products.flatMap((product) => product.meters); | |
| 127 | 129 | const sum = (keys: readonly string[], pick: (meter: MeterLine) => number) => | |
| 128 | 130 | 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); | |
| 129 | 132 | return { | |
| 130 | 133 | from: report.from, | |
| 131 | 134 | 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)), | |
| 134 | 138 | ownSeconds: sum([OWN_METER], (meter) => meter.quantity), | |
| 135 | 139 | free: report.free, | |
| 136 | 140 | }; | |
| 137 | 141 | } | |
| 138 | 142 | ||
| 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 | + | })); | |
| 152 | 150 | } | |
| 153 | 151 | ||
| 154 | 152 | /** Seconds as people read machine time: "45s", "12 min", "3.4 h". */ | |
| 58 | 58 | test("quantities read in their units", () => { | |
| 59 | 59 | assert.equal(quantity(1_234_567, "tokens"), "1.2M tokens"); | |
| 60 | 60 | 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"); | |
| 61 | 63 | assert.equal(quantity(504_000_000, "bytes"), "504 MB"); | |
| 62 | 64 | assert.equal(quantity(1, "entries"), "1 entry"); | |
| 63 | 65 | }); |
| 192 | 192 | const m = Math.floor((s % 3600) / 60); | |
| 193 | 193 | return h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s % 60}s` : `${s}s`; | |
| 194 | 194 | } | |
| 195 | + | case "minutes": | |
| 196 | + | return `${compact(amount)} ${Math.round(amount) === 1 ? "minute" : "minutes"}`; | |
| 195 | 197 | case "bytes": | |
| 196 | 198 | return bytes(amount); | |
| 197 | 199 | case "operations": |
| 1 | 1 | import { ArrowUpRight } from "lucide-react"; | |
| 2 | 2 | ||
| 3 | 3 | import type { FeaturePlan, FreeTier, Price } from "@g1t/contracts"; | |
| 4 | + | import { type InstanceType, machineSize, machines } from "@g1t/contracts/instances"; | |
| 4 | 5 | ||
| 5 | 6 | import type { Route } from "./+types/pricing"; | |
| 6 | 7 | import { money as sum, wholeDollars } from "../lib/money"; | |
| ⋯ | |||
| 64 | 65 | "card_fee_percent", | |
| 65 | 66 | "card_fee_fixed", | |
| 66 | 67 | "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", | |
| 67 | 73 | ]); | |
| 68 | 74 | ||
| 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 | + | ||
| 69 | 84 | /** A retired meter: agents' models and tokens, and the AI Gateway. */ | |
| 70 | 85 | function retired(meter: string): boolean { | |
| 71 | 86 | return meter.startsWith("agent_") || meter === "gateway_models"; | |
| ⋯ | |||
| 148 | 163 | const HOW = [ | |
| 149 | 164 | { | |
| 150 | 165 | 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.", | |
| 152 | 167 | }, | |
| 153 | 168 | { | |
| 154 | 169 | title: "Prices follow costs, by themselves", | |
| ⋯ | |||
| 612 | 627 | </> | |
| 613 | 628 | ); | |
| 614 | 629 | })()} | |
| 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 | + | ))} | |
| 615 | 645 | <tr> | |
| 616 | 646 | <td className="px-4 py-3"> | |
| 617 | 647 | <p className="font-medium">Your own runners</p> | |
Comments
Sign in to comment.