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

g1t's runners use only Cloudflare's named instance types, sized for the least they cost while giving a job what it needs: the standard machine (ubuntu-latest, every other hosted label and all other work) is standard-3, 2 vCPU, 8 GiB and 16 GB of disk, where a custom 1 vCPU, 4 GiB, 20 GB type had been asked for and standard-1's half vCPU, 4 GiB and 8 GB was still being served; g1t-2core is kept as a name of the same machine, so workflows that ask for it keep working, at the same price; and g1t-4core stays standard-4, 4 vCPU, 12 GiB and 20 GB, for heavier jobs. At the classes' max_instances (20, 10, 10) the runners hold at most 100 vCPU, 360 GiB and 680 GB at once, well inside the account's 1,500 vCPU and 6 TiB. The contracts' instance table and its TypeScript mirror say the same sizes; the price scale, worked out as before from Cloudflare's list prices with every vCPU busy and the Durable Object behind each, is 1.8 for g1t-4core against the standard machine. The billing keeper's base is standard-3's memory, disk and Durable Object (22.6825 millionths of a dollar a second, billed whether the CPU is busy or not), and the fallbacks follow it: 63 for a sandbox second with both vCPUs busy, 26 for a build second. Migration 0056 moves the price book to the new base at once rather than after the notice period, so g1t never charges less than a sandbox costs it: sandbox_base_second to 22.6825, and sandbox_second and build_second up by the base's growth of 10.56, replacing any rise still waiting for its date. Tests hold wrangler.jsonc to the contracts' named sizes, the scales to the list prices, and every machine's price scale to at least its base's, so an idle job that does not report its CPU still pays for the memory and disk it held. The docs' machine tables, prices and the Cloudflare deploy guide say so.

syntaqxcommitted Parent3a656dcBrowse files
16 files+235−1210/16 viewed
+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
+10−12
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
543542 | `runs-on` | vCPUs | Memory | Disk |
544543 | --- | --- | --- | --- |
545−| `ubuntu-latest`, or any other Linux label | 1 | 4 GiB | 20 GB |
546−| `g1t-2core` | 2 | 8 GiB | 20 GB |
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 |
547546 | `g1t-4core` | 4 | 12 GiB | 20 GB |
548547
549548 ```yaml
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' }}`). What
559+each costs is in [usage and billing](/guides/usage-and-billing/#machines).
561560
562561 A job on g1t's machines runs for at most 60 minutes, whatever its
563562 `timeout-minutes`, and the workflow's page warns about a job that asks
751750
752751 - **Machine.** Containers share the job's machine: its vCPUs, memory and
753752 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.
753+ `g1t-4core`. `--cpus` and `--memory` limit a container within that.
756754 - **Disk.** Images take room on the job's disk. On a machine whose disk
757755 cannot hold layered images, the Engine stores plain copies, which take
758756 more room; the log says when it does.
+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.
+26−18
139139
140140 | What | Unit | Costs g1t | You pay |
141141 | --- | --- | --- | --- |
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 |
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 |
144+| Deploy builds | Second | About $0.0015 a minute | About $0.0018 a minute |
145145 | App requests | Million | $0.30 | $0.36 |
146146 | App CPU time | Million CPU milliseconds | $0.02 | $0.024 |
147147 | Custom domains | Domain-month | $0.10 | $0.12 |
260260 | | Charged |
261261 | --- | --- |
262262 | 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) |
263+| [Workflow](/guides/actions/) jobs | [Sandbox time](#sandbox-time), more on [`g1t-4core`](#machines) |
264264 | [Actions cache](#actions-cache) | What it holds, on the plan only |
265265 | [Deployments](/guides/deployments/) | Builds, requests, CPU time, custom domains and container time, from the first. Projects and previews are not charged. |
266266 | [Private repository storage](#storage-search-embeddings-and-scans) | Past the free 1 GB, on the plan only |
327327 days later, and for something charged once a month (git operations,
328328 storage, the actions cache, custom domains, embeddings, scans) at the
329329 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.
330334 - **Nothing already charged changes.** Each charge records the price it
331335 was made at.
332336
352356 counted here: [Deployments](/guides/deployments/) charges them by the
353357 second, from the first, under **Builds**.
354358
355−### Workflow jobs on larger machines
359+### Machines
356360
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:
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.
361366
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 |
367+| `runs-on` | Machine | Memory, disk and the Durable Object, billed every second | CPU, billed as used | A second without its own CPU reported |
368+| --- | --- | --- | --- | --- |
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 |
366371
367−A job that reports its own CPU is priced on it: its memory and disk for
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
368374 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.
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.
373381
374382 ## Git operations
375383
+27−19
10371037 pub brokered: bool,
10381038 }
10391039
1040−/// A size of machine g1t runs workflow jobs on, asked for by a label in
1041−/// `runs-on`. Each is a Cloudflare Containers instance type (named, or a
1042−/// custom one: services/runner/wrangler.jsonc), every one with the 20 GB of
1043−/// disk Containers allow at most; it costs what that instance costs g1t,
1044−/// plus the margin, like any sandbox time.
1040+/// A machine g1t runs workflow jobs on, asked for by a label in `runs-on`.
1041+/// Each is a named Cloudflare Containers instance type (see
1042+/// services/runner/wrangler.jsonc; no custom types): the standard machine
1043+/// is standard-3 (2 vCPU, 8 GiB, 16 GB of disk), `g1t-2core` is kept as a
1044+/// name of it so workflows that ask for it keep working, and `g1t-4core` is
1045+/// standard-4 (4 vCPU, 12 GiB, 20 GB). It costs what that instance costs
1046+/// g1t, plus the margin, like any sandbox time.
10451047 #[derive(Clone, Copy, Debug, PartialEq)]
10461048 pub struct InstanceType {
10471049 /// The `runs-on` label, or `standard` for the default.
10481050 pub label: &'static str,
1049− /// The Containers instance type: a named one, or `custom` for one
1050− /// sized by its vCPU, memory and disk.
1051+ /// The named Containers instance type.
10511052 pub container: &'static str,
10521053 pub vcpu: f64,
10531054 pub memory_gib: f64,
10631064
10641065 /// The default: what `ubuntu-latest` and every other hosted label get.
10651066 pub const STANDARD_INSTANCE: InstanceType =
1066− InstanceType { label: "standard", container: "custom", vcpu: 1.0, memory_gib: 4.0, disk_gb: 20.0, price_scale: 1.0 };
1067+ InstanceType { label: "standard", container: "standard-3", vcpu: 2.0, memory_gib: 8.0, disk_gb: 16.0, price_scale: 1.0 };
10671068
1068−/// Every machine a workflow job can ask for, the default first.
1069+/// Every label a workflow job can ask for, the default first.
10691070 pub const INSTANCE_TYPES: [InstanceType; 3] = [
10701071 STANDARD_INSTANCE,
1071− InstanceType { label: "g1t-2core", container: "custom", vcpu: 2.0, memory_gib: 8.0, disk_gb: 20.0, price_scale: 1.9 },
1072− InstanceType { label: "g1t-4core", container: "standard-4", vcpu: 4.0, memory_gib: 12.0, disk_gb: 20.0, price_scale: 3.4 },
1072+ // The standard machine under the name it had when it was larger.
1073+ InstanceType { label: "g1t-2core", ..STANDARD_INSTANCE },
1074+ InstanceType { label: "g1t-4core", container: "standard-4", vcpu: 4.0, memory_gib: 12.0, disk_gb: 20.0, price_scale: 1.8 },
10731075 ];
10741076
10751077 /// The machine a job's `runs-on` labels ask for: the largest named, or the
15831585 fn runs_on_picks_the_machine() {
15841586 assert_eq!(instance_for(&labels(&["ubuntu-latest"])), STANDARD_INSTANCE);
15851587 assert_eq!(instance_for(&labels(&[])).label, "standard");
1586− assert_eq!(instance_for(&labels(&["g1t-4core"])).container, "standard-4");
1588+ assert_eq!(instance_for(&labels(&["g1t-4core"])).label, "g1t-4core");
15871589 assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"])).label, "g1t-2core");
1588− // Both named: the larger.
1590+ // Both named: the later one.
15891591 assert_eq!(instance_for(&labels(&["g1t-2core", "g1t-4core"])).label, "g1t-4core");
1592+ assert_eq!(instance_named("g1t-2core").map(|i| i.vcpu), Some(2.0));
15901593 assert_eq!(instance_named("g1t-4core").map(|i| i.vcpu), Some(4.0));
15911594 assert_eq!(instance_named("standard"), Some(STANDARD_INSTANCE));
15921595 assert_eq!(instance_named("g1t-64core"), None);
15931596 }
15941597
15951598 #[test]
1596− fn every_machine_has_the_most_disk_containers_allow_and_fits_a_custom_type() {
1599+ fn every_machine_is_a_named_instance_type_at_its_size() {
1600+ // Containers' named instance types, at the sizes Cloudflare gives
1601+ // them; no custom types.
1602+ let named = |container: &str| match container {
1603+ "standard-3" => Some((2.0, 8.0, 16.0)),
1604+ "standard-4" => Some((4.0, 12.0, 20.0)),
1605+ _ => None,
1606+ };
15971607 for instance in INSTANCE_TYPES {
1598− assert_eq!(instance.disk_gb, 20.0, "{}", instance.label);
1599− // Custom instance types: 1 to 4 vCPU, at most 12 GiB, at least
1600− // 3 GiB a vCPU.
1601− assert!((1.0..=4.0).contains(&instance.vcpu), "{}", instance.label);
1602− assert!(instance.memory_gib <= 12.0 && instance.memory_gib >= 3.0 * instance.vcpu, "{}", instance.label);
1608+ assert_eq!(named(instance.container), Some((instance.vcpu, instance.memory_gib, instance.disk_gb)), "{}", instance.label);
16031609 }
1610+ // g1t-2core is a name of the standard machine.
1611+ assert_eq!(instance_named("g1t-2core").map(|i| InstanceType { label: "standard", ..i }), Some(STANDARD_INSTANCE));
16041612 }
16051613
16061614 #[test]
+6−4
387387 pub const MICROS_PER_MILLION_CPU_MS: i64 = 20_000;
388388 /// What one second of a build's sandbox costs g1t (the standard
389389 /// sandbox, `actions::STANDARD_INSTANCE`), rounded up: the price keeper
390− /// measured 14.5 on 2026-10-05, when it had 8 GB of disk, and its 20 GB
391− /// now add 0.84. Only a fallback: billing charges builds at the price
392− /// book's `build_second`, which the keeper keeps current.
393− pub const MICROS_PER_BUILD_SECOND: i64 = 16;
390+ /// measured 14.5 on 2026-10-05, when it had 4 GiB and 8 GB of disk
391+ /// (12.1225 of it memory, disk and the Durable Object), and standard-3's
392+ /// 8 GiB and 16 GB (22.6825) add 10.56, for the same CPU in use. Only a
393+ /// fallback: billing charges builds at the price book's `build_second`,
394+ /// which the keeper keeps current.
395+ pub const MICROS_PER_BUILD_SECOND: i64 = 26;
394396 /// What one custom hostname costs g1t a month (Cloudflare for SaaS):
395397 /// $0.10.
396398 pub const MICROS_PER_DOMAIN_MONTH: i64 = 100_000;
+4−4
249249
250250 /**
251251 * What one second of a sandbox costs g1t, in millionths of a dollar, when
252− * the price book cannot be read: the standard sandbox (1 vCPU, 4 GiB, 20 GB)
253− * with its vCPU busy and its Durable Object, 32.96 at list prices, rounded
254− * up.
252+ * the price book cannot be read: the standard sandbox (standard-3: 2 vCPU,
253+ * 8 GiB, 16 GB) with both vCPUs busy and its Durable Object, 62.68 at list
254+ * prices, rounded up.
255255 */
256−export const FALLBACK_SANDBOX_MICROS_PER_SECOND = 33;
256+export const FALLBACK_SANDBOX_MICROS_PER_SECOND = 63;
257257
258258 /** A sandbox for `minutes` at `microsPerSecond`. */
259259 export function sandboxEstimateMicros(minutes: number, microsPerSecond: number): number {
+17−14
11 /**
2− * Sizes of machine g1t runs workflow jobs on, asked for by a label in
3− * `runs-on`. Mirrors `g1t_contracts::actions::INSTANCE_TYPES`. Each is a
4− * Cloudflare Containers instance type (named, or a custom one sized in
5− * services/runner/wrangler.jsonc), every one with the 20 GB of disk
6− * Containers allow at most, and costs what that instance costs g1t, plus
7− * the margin, like any sandbox time.
2+ * Machines g1t runs workflow jobs on, asked for by a label in `runs-on`.
3+ * Mirrors `g1t_contracts::actions::INSTANCE_TYPES`. Each is a named
4+ * Cloudflare Containers instance type (see services/runner/wrangler.jsonc;
5+ * no custom types): the standard machine is standard-3 (2 vCPU, 8 GiB,
6+ * 16 GB of disk), `g1t-2core` is kept as a name of it so workflows that ask
7+ * for it keep working, and `g1t-4core` is standard-4 (4 vCPU, 12 GiB,
8+ * 20 GB). It costs what that instance costs g1t, plus the margin, like any
9+ * sandbox time.
810 */
911 export type InstanceType = {
1012 /** The `runs-on` label, or `standard` for the default. */
1113 label: string;
12− /** The Containers instance type: a named one, or `custom`. */
14+ /** The named Containers instance type. */
1315 container: string;
1416 vcpu: number;
1517 memoryGib: number;
2628 /** The default: what `ubuntu-latest` and every other hosted label get. */
2729 export const STANDARD_INSTANCE: InstanceType = {
2830 label: "standard",
29− container: "custom",
30− vcpu: 1,
31− memoryGib: 4,
32− diskGb: 20,
31+ container: "standard-3",
32+ vcpu: 2,
33+ memoryGib: 8,
34+ diskGb: 16,
3335 estimateScale: 1,
3436 };
3537
3638 /**
37− * Every machine a job can ask for, the default first. The scales follow
39+ * Every label a job can ask for, the default first. The scales follow
3840 * Cloudflare's list prices: memory $0.0000025 a GiB-second, disk
3941 * $0.00000007 a GB-second, vCPU $0.00002 a second, and the Durable Object
4042 * behind each at 128 MB, $0.0000125 a GB-second; to one decimal.
4143 */
4244 export const INSTANCE_TYPES: readonly InstanceType[] = [
4345 STANDARD_INSTANCE,
44− { label: "g1t-2core", container: "custom", vcpu: 2, memoryGib: 8, diskGb: 20, estimateScale: 1.9 },
45− { label: "g1t-4core", container: "standard-4", vcpu: 4, memoryGib: 12, diskGb: 20, estimateScale: 3.4 },
46+ // The standard machine under the name it had when it was larger.
47+ { ...STANDARD_INSTANCE, label: "g1t-2core" },
48+ { label: "g1t-4core", container: "standard-4", vcpu: 4, memoryGib: 12, diskGb: 20, estimateScale: 1.8 },
4649 ];
4750
4851 /** An instance type by its label; the standard one for anything else. */
+66−0
1+-- The standard sandbox is now Cloudflare Containers' standard-3: 2 vCPU,
2+-- 8 GiB of memory and 16 GB of disk, where it had half a vCPU, 4 GiB and
3+-- 8 GB (standard-1; a custom 1 vCPU, 4 GiB, 20 GB type was never served).
4+-- Cloudflare bills memory and disk as provisioned for every second a
5+-- container runs, so a sandbox second's base (memory, disk and the Durable
6+-- Object behind it) is now, at list prices:
7+--
8+-- 8 x 2.5 + 16 x 0.07 + 0.125 x 12.5 = 22.6825 millionths of a dollar
9+--
10+-- against 12.1225 for 4 GiB and 8 GB. g1t-4core (standard-4) is priced
11+-- from it by its scale in the contracts. The price book follows it now,
12+-- not after the keeper's notice period, so g1t never charges less than a
13+-- sandbox costs it:
14+--
15+-- - sandbox_base_second (runs that report their own CPU): 22.6825.
16+-- - sandbox_second and build_second (the average, for runs that do not):
17+-- what they were, or a rise still waiting for its date if that is more,
18+-- plus 10.56, the base's growth from 12.1225. The CPU in use on top of
19+-- the base is unchanged, and counting it from the smallest base the
20+-- price book had keeps the result at or above cost.
21+--
22+-- Rises still waiting for their date on these meters are replaced, as
23+-- pricing.rs does when a newer decision replaces a waiting version, so
24+-- none can take the price below cost later. The keeper measures again on
25+-- its daily run.
26+
27+-- 1. What changes, and why.
28+INSERT OR IGNORE INTO price_changes (id, meter, old_cost_micros, new_cost_micros, markup_percent, reason, created_at)
29+SELECT 'prc_standard_3_' || meter, meter, cost_micros,
30+ CASE meter
31+ WHEN 'sandbox_base_second' THEN MAX(cost_micros, 22.6825,
32+ COALESCE((SELECT MAX(v.cost_micros) FROM price_versions v WHERE v.meter = prices.meter AND v.applied_at IS NULL), 0))
33+ ELSE MAX(cost_micros,
34+ COALESCE((SELECT MAX(v.cost_micros) FROM price_versions v WHERE v.meter = prices.meter AND v.applied_at IS NULL), 0)) + 10.56
35+ END,
36+ markup_percent,
37+ 'Higher: the standard sandbox is now 2 vCPU, 8 GiB and 16 GB, and Cloudflare bills its memory and disk for every second it runs',
38+ strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
39+ FROM prices WHERE meter IN ('sandbox_base_second', 'sandbox_second', 'build_second');
40+
41+-- 2. The price book, now.
42+UPDATE prices
43+ SET cost_micros = (SELECT c.new_cost_micros FROM price_changes c WHERE c.id = 'prc_standard_3_' || prices.meter),
44+ updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
45+ WHERE meter IN ('sandbox_base_second', 'sandbox_second', 'build_second')
46+ AND EXISTS (SELECT 1 FROM price_changes c WHERE c.id = 'prc_standard_3_' || prices.meter);
47+
48+-- 3. Rises waiting for their date are replaced.
49+UPDATE price_proposals SET status = 'superseded'
50+ WHERE version_id IN (SELECT id FROM price_versions
51+ WHERE meter IN ('sandbox_base_second', 'sandbox_second', 'build_second') AND applied_at IS NULL);
52+
53+DELETE FROM price_versions
54+ WHERE meter IN ('sandbox_base_second', 'sandbox_second', 'build_second') AND applied_at IS NULL;
55+
56+-- 4. A version in force from now, so the charges after it are explained by
57+-- it.
58+INSERT OR IGNORE INTO price_versions (id, meter, version, cost_micros, markup_percent, effective_at, reason, created_by, created_at, applied_at)
59+SELECT 'pv_' || p.meter || '_' || (COALESCE((SELECT MAX(v.version) FROM price_versions v WHERE v.meter = p.meter), 0) + 1),
60+ p.meter,
61+ COALESCE((SELECT MAX(v.version) FROM price_versions v WHERE v.meter = p.meter), 0) + 1,
62+ p.cost_micros, p.markup_percent,
63+ strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
64+ 'The standard sandbox is now 2 vCPU, 8 GiB and 16 GB', 'migration',
65+ strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
66+ FROM prices p WHERE p.meter IN ('sandbox_base_second', 'sandbox_second', 'build_second');
+3−3
854854
855855 #[test]
856856 fn every_build_second_is_metered_at_cost_plus_the_margin() {
857− // A 5-minute build at 16 millionths a second costs g1t 4,800, and
857+ // A 5-minute build at 26 millionths a second costs g1t 7,800, and
858858 // is charged at cost plus 20%, from the first second: there are no
859859 // included build minutes, only the plan's included usage.
860860 let cost = 300 * costs::MICROS_PER_BUILD_SECOND;
861− assert_eq!(cost, 4_800);
862− assert_eq!(crate::credits::with_margin(cost, 20), 5_760);
861+ assert_eq!(cost, 7_800);
862+ assert_eq!(crate::credits::with_margin(cost, 20), 9_360);
863863 }
864864
865865 #[test]
+35−15
187187 memory_byte_seconds: f64,
188188 }
189189
190−/// g1t's standard sandbox (`STANDARD_INSTANCE` in the contracts): a custom
191−/// Containers instance type of 1 vCPU, 4 GiB and 20 GB of disk.
192−const SANDBOX_GIB: f64 = 4.0;
193−const SANDBOX_DISK_GB: f64 = 20.0;
190+/// g1t's standard sandbox (`STANDARD_INSTANCE` in the contracts):
191+/// Containers' standard-3, 2 vCPU, 8 GiB and 16 GB of disk. Memory and disk
192+/// are billed as provisioned for every second it runs, so they are in the
193+/// base; CPU only as used.
194+const SANDBOX_GIB: f64 = 8.0;
195+const SANDBOX_DISK_GB: f64 = 16.0;
194196 const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
195197 /// The Durable Object behind each container is billed for as long as the
196198 /// container runs, at 128 MB.
563565 #[test]
564566 fn a_sandbox_second_is_its_memory_and_disk_and_the_cpu_it_uses() {
565567 // An hour of sandboxes that kept a fifth of a vCPU busy.
566− let usage = ContainerUsage { cpu_seconds: 720.0, memory_byte_seconds: 3600.0 * 4.0 * GIB };
568+ let usage = ContainerUsage { cpu_seconds: 720.0, memory_byte_seconds: 3600.0 * 8.0 * GIB };
567569 let micros =
568570 sandbox_second_micros(usage, LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_VCPU_SECOND, LIST_DO_GB_SECOND).unwrap();
569− // 4 x 2.5 + 20 x 0.07 + 0.125 x 12.5 + 0.2 x 20 = 16.9625
570− assert!((micros - 16.9625).abs() < 1e-9, "{micros}");
571− // The Durable Object adds about 14% to the second it left out.
572− assert!((sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND) - 12.9625).abs() < 1e-9);
571+ // 8 x 2.5 + 16 x 0.07 + 0.125 x 12.5 + 0.2 x 20 = 26.6825
572+ assert!((micros - 26.6825).abs() < 1e-9, "{micros}");
573+ // Memory, disk and the Durable Object, billed for every second.
574+ let base = sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND);
575+ assert!((base - 22.6825).abs() < 1e-9);
576+ // An hour of sandboxes that used no CPU still costs their base.
577+ let idle = ContainerUsage { cpu_seconds: 0.0, memory_byte_seconds: 3600.0 * 8.0 * GIB };
578+ let idle = sandbox_second_micros(idle, LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_VCPU_SECOND, LIST_DO_GB_SECOND).unwrap();
579+ assert!((idle - base).abs() < 1e-9);
573580 // Too little use to say anything.
574581 assert!(sandbox_second_micros(ContainerUsage { cpu_seconds: 1.0, memory_byte_seconds: GIB }, 1.0, 1.0, 1.0, 1.0).is_none());
575582 }
578585 fn a_run_that_reports_its_cpu_is_priced_on_it() {
579586 let base = sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND);
580587 let vcpu = LIST_VCPU_SECOND * MICROS_PER_DOLLAR as f64;
581− // A 10-minute cargo build that kept half its vCPU busy throughout.
588+ // A 10-minute cargo build that kept half a vCPU busy throughout.
582589 let heavy = run_cost(600, 300.0, base, vcpu);
583− assert!((heavy - (600.0 * 12.9625 + 300.0 * 20.0)).abs() < 1e-6);
590+ assert!((heavy - (600.0 * 22.6825 + 300.0 * 20.0)).abs() < 1e-6);
591+ // Ten idle minutes cost the memory, disk and Durable Object that
592+ // Cloudflare bills for all of them, and are charged that plus the
593+ // margin, never less.
594+ let idle = run_cost(600, 0.0, base, vcpu);
595+ let provisioned = 600.0 * (8.0 * 2.5 + 16.0 * 0.07 + 0.125 * 12.5);
596+ assert!((idle - provisioned).abs() < 1e-6, "{idle}");
597+ let charged = crate::credits::with_margin(idle.ceil() as i64, 20);
598+ assert!(charged as f64 >= provisioned * 1.2, "{charged}");
584599 // The same ten minutes, mostly idle, costs less.
585600 let light = run_cost(600, 30.0, base, vcpu);
586601 assert!(light < heavy);
590605 assert_eq!(run_cost(0, -1.0, base, vcpu), 0.0);
591606 // A larger machine's base: its memory and disk, not its CPU.
592607 assert!((base_scale(SANDBOX_GIB, SANDBOX_DISK_GB) - 1.0).abs() < 1e-12);
593− assert!((base_scale(12.0, 20.0) - 2.54).abs() < 0.01, "{}", base_scale(12.0, 20.0));
594− assert!((base_scale(8.0, 20.0) - 1.77).abs() < 0.01, "{}", base_scale(8.0, 20.0));
595− // Each machine in the contracts, priced from its own memory and disk.
608+ assert!((base_scale(12.0, 20.0) - 1.45).abs() < 0.01, "{}", base_scale(12.0, 20.0));
609+ // Each machine in the contracts, priced from its own memory and
610+ // disk. Its price scale is at least its base's, so a job that does
611+ // not report its CPU (charged the average sandbox second, which is
612+ // never under the base, times the price scale) still pays for the
613+ // memory and disk it held, idle or not.
596614 for instance in g1t_contracts::actions::INSTANCE_TYPES {
597615 let scale = base_scale(instance.memory_gib, instance.disk_gb);
598− assert!(scale >= 1.0 && scale < instance.price_scale.max(1.0) + 1e-9, "{}: {scale}", instance.label);
616+ assert!(scale >= 1.0 - 1e-12 && scale <= instance.price_scale + 1e-9, "{}: {scale}", instance.label);
599617 }
618+ let standard = g1t_contracts::actions::STANDARD_INSTANCE;
619+ assert_eq!((standard.memory_gib, standard.disk_gb), (SANDBOX_GIB, SANDBOX_DISK_GB));
600620 }
601621
602622 #[test]
+1−1
228228 const gate = new ComputeGate(billing({}).binding, memory(), quiet);
229229 assert.equal(await gate.microsPerSecond(), 20);
230230 const down = new ComputeGate(billing({ fail: new Set(["prices"]) }).binding, memory(), quiet);
231− assert.equal(await down.microsPerSecond(), 33);
231+ assert.equal(await down.microsPerSecond(), 63);
232232 });
233233
234234 // ---- Caps -------------------------------------------------------------------------------
+3−2
6868 export interface RunnerEnv {
6969 SANDBOX: DurableObjectNamespace<AttemptSandbox>;
7070 /**
71− * Larger machines for workflow jobs that ask for one with `runs-on`
72− * (`g1t-2core`, `g1t-4core`): the same image on a larger instance type.
71+ * Workflow jobs that ask with `runs-on` for `g1t-2core` (the standard
72+ * machine, kept as a name so the label keeps working) or `g1t-4core` (a
73+ * larger instance type): the same image, in classes of their own.
7374 */
7475 SANDBOX_2CORE?: DurableObjectNamespace<Sandbox2Core>;
7576 SANDBOX_4CORE?: DurableObjectNamespace<Sandbox4Core>;
+9−5
5050 type Size = { vcpu: number; memory_mib: number; disk_mb: number };
5151 // wrangler.jsonc's comments are whole lines.
5252 const text = readFileSync(new URL("../wrangler.jsonc", import.meta.url), "utf8").replace(/^\s*\/\/.*$/gm, "");
53− const config = JSON.parse(text) as { containers: { class_name: string; instance_type: string | Size }[] };
54− // The named Cloudflare instance types a machine may be.
55− const named: Record<string, Size> = { "standard-4": { vcpu: 4, memory_mib: 12 * 1024, disk_mb: 20_000 } };
53+ const config = JSON.parse(text) as { containers: { class_name: string; instance_type: unknown }[] };
54+ // The named Cloudflare instance types a machine may be, at the sizes
55+ // Cloudflare gives them. No custom instance types.
56+ const named: Record<string, Size> = {
57+ "standard-3": { vcpu: 2, memory_mib: 8 * 1024, disk_mb: 16_000 },
58+ "standard-4": { vcpu: 4, memory_mib: 12 * 1024, disk_mb: 20_000 },
59+ };
5660 const classes = Object.keys(SANDBOX_BINDINGS);
5761 assert.equal(INSTANCE_TYPES.length, classes.length);
5862 INSTANCE_TYPES.forEach((instance, i) => {
5963 const type = config.containers.find((c) => c.class_name === classes[i])?.instance_type;
60− const size = typeof type === "string" ? named[type] : type;
64+ const size = typeof type === "string" ? named[type] : undefined;
6165 assert.ok(size, `${classes[i]}: ${JSON.stringify(type)}`);
62− assert.equal(typeof type === "string" ? type : "custom", instance.container, instance.label);
66+ assert.equal(type, instance.container, instance.label);
6367 assert.deepEqual(
6468 { vcpu: size.vcpu, memoryGib: size.memory_mib / 1024, diskGb: size.disk_mb / 1000 },
6569 { vcpu: instance.vcpu, memoryGib: instance.memoryGib, diskGb: instance.diskGb },
+11−9
55 "compatibility_date": "2026-09-26",
66 "main": "./src/index.ts",
77 "workers_dev": false,
8− // One image (the base plus the runner binary), on three machine sizes,
9− // each with the 20 GB of disk Containers allow at most. The standard and
10− // g1t-2core machines are custom instance types (1 vCPU is the smallest a
11− // custom type may have, with at least 3 GiB of memory per vCPU), since
12− // the named ones that size come with 8 and 16 GB. crates/contracts
8+ // One image (the base plus the runner binary), on two named instance
9+ // types: standard-3 (2 vCPU, 8 GiB, 16 GB of disk) for the standard
10+ // machine and g1t-2core, which is kept as a name of it so workflows that
11+ // ask for it keep working, and standard-4 (4 vCPU, 12 GiB, 20 GB) for
12+ // g1t-4core. At these max_instances, 40 sandboxes at once hold 100 vCPU,
13+ // 360 GiB and 680 GB, well inside the account's limits for live
14+ // instances (1,500 vCPU, 6 TiB of memory). crates/contracts
1315 // (INSTANCE_TYPES) and services/billing (keeper.rs) say the same sizes.
1416 // scripts/deploy.mjs deploys with each image set to the reference it
1517 // built and pushed ("The runner's images" in
2022 "class_name": "AttemptSandbox",
2123 "image": "./Dockerfile",
2224 // Workflow jobs with `runs-on: ubuntu-latest` (and every other
23− // hosted label), and all other work: 1 vCPU, 4 GiB, 20 GB.
24− "instance_type": { "vcpu": 1, "memory_mib": 4096, "disk_mb": 20000 },
25+ // hosted label), and all other work: 2 vCPU, 8 GiB, 16 GB.
26+ "instance_type": "standard-3",
2527 "max_instances": 20,
2628 // A deploy replaces sandboxes. Ones that are busy are given this
2729 // long, in seconds, to finish first, so shipping g1t does not
2931 "rollout_active_grace_period": 7200
3032 },
3133 {
32− // Workflow jobs with `runs-on: g1t-2core`: 2 vCPU, 8 GiB, 20 GB.
34+ // Workflow jobs with `runs-on: g1t-2core`: the standard machine.
3335 "class_name": "Sandbox2Core",
3436 "image": "./Dockerfile",
35− "instance_type": { "vcpu": 2, "memory_mib": 8192, "disk_mb": 20000 },
37+ "instance_type": "standard-3",
3638 "max_instances": 10,
3739 "rollout_active_grace_period": 7200
3840 },

Comments

Sign in to comment.