JarvisOS HomeLab status
No signal

Elastic burst compute

6 targets · 5 transient classes · plan proposed

HOST-004. Elastic burst compute policy and the provider adapter plan. Nothing here provisions anything: the console renders this register and holds no provider credential, and scripts/jarvisos-burst.mjs refuses every billable verb without an owner authorisation grant. Measurements marked 'measured' were taken on VIN on 2026-08-17; every price is arithmetic on a number nobody has yet checked against an invoice, which is gap burst-prices-unverified.

This page provisions nothing and holds no provider credential. It renders the register and the arithmetic derived from it; scripts/jarvisos-burst.mjs is the only thing that could call a provider, and it refuses every billable verb without an owner authorisation grant.

Recurring, per month €5.00 the VPS the estate already pays for
Variable, per month €0.24 projected against a €10.00 ceiling
Transient hours 133.5 h across 5 workload classes
Past break-even 0/6 burst is still the cheap answer
Needs a person 8/17 9 run unattended
Credentials present 0/4 nothing here can spend anything today

The comparison

scored 0–4 on each axis, derived from declared facts

Cost is scored at projected utilisation, not at list price: a machine is only expensive in proportion to how much of it you buy. Targets nothing routes work to are scored at a 20 h/month reference so that being idle is not mistaken for being cheap.

Target Decision Hours Per month Total monthly costStartup latencyAPI controllabilitySnapshotsBandwidthTailscale attach Weighted
VIN as it is permanent · hetzner · vin Adopted 0.0 h scored at 20 h €5.00 unverified 2 4 3 3 4 4 3.15
VIN upgraded to CPX31 permanent · hetzner Rejected 0.0 h scored at 20 h €15.59 unverified 0 4 3 3 4 4 2.55
Home1 scratch worker owned · self · home1 Adopted 108.0 h €0.00 measured 4 3 2 2 4 4 3.30
Hetzner CPX41 burst node elastic · hetzner-cloud Standby 5.5 h €0.24 unverified 3 2 4 3 4 3 3.05
Hetzner CAX31 burst node (ARM) elastic · hetzner-cloud Standby 0.0 h scored at 20 h €0.00 unverified 3 2 4 3 4 3 3.05
GitHub Actions hosted runner managed · github Standby 20.0 h €0.00 unverified 4 2 2 0 4 3 2.75

Weights: Total monthly cost 0.3 · Startup latency 0.2 · API controllability 0.15 · Snapshots 0.1 · Bandwidth 0.1 · Tailscale attach 0.15. The six axes the task named. Cost leads because the estate's stated optimisation is zero recurring cost; startup and mesh attach are next because a burst target that takes four minutes to become reachable is not usable for interactive work whatever it costs. Snapshots and bandwidth are real but rarely decide anything at this scale.

Rejected, and why

  • VIN upgraded to CPX31 — It buys 720 hours a month of capacity to serve roughly 34 hours of transient work, and it buys them every month forever. At the projected utilisation the same work costs under a euro on demand. It also fails to fix the thing that is actually wrong: the memory pressure on VIN is caused by workloads the placement plan already moves to home1, so the upgrade would pay to keep them in the wrong place.

Rejected options stay in the register, keep being priced and keep being scored. An option that disappears when it loses stops being evidence that a choice was made.

Break-even

where elastic stops being the cheap answer

Derived, not declared. Above the line the honest recommendation is the permanent machine — which is the one claim that makes this a policy rather than an argument for elastic compute.

Elastic option Against Break-even Projected Verdict
Hetzner CPX41 burst node VIN as it is €5.00/mo 115 h 5.5 h below it
Hetzner CPX41 burst node VIN upgraded to CPX31 €15.59/mo 360 h 5.5 h below it
Hetzner CAX31 burst node (ARM) VIN as it is €5.00/mo 388 h 0.0 h below it
Hetzner CAX31 burst node (ARM) VIN upgraded to CPX31 €15.59/mo never — cheaper at any utilisation 0.0 h below it
GitHub Actions hosted runner VIN as it is €5.00/mo 44 h 20.0 h below it
GitHub Actions hosted runner VIN upgraded to CPX31 €15.59/mo 68 h 20.0 h below it

Who signs for what

9 automatic · 8 owner-gated

Read-only inventory and health run unattended because they are free and cannot change anything. Everything that provisions, resizes, rebuilds, snapshots or deletes spends money or destroys something, and every one of those requires a scoped, budget-capped, expiring, single-use grant issued by the owner.

Runs unattended

An operation may run unattended if and only if its effect is 'read' and it is not billable. There is no third condition and no exception list.

  • inventory: which burst resources exist right now, in which project, with which labels
  • health: power state, uptime and metrics of a running burst node
  • pricing: the provider's own price list, which is how every euro on this page gets verified
  • usage: minutes and traffic consumed so far this month, which is how the ceiling is watched
  • estimate: arithmetic performed locally on the numbers above

Requires the owner

Any operation that is billable, or whose effect is create, mutate or destroy, requires a valid grant. Declaring such an operation automatic fails npm run check.

  • provision: create a server, a volume or a floating address
  • resize: change a server type, in either direction — scaling down still bills the change
  • rebuild: replace a running server's image
  • snapshot: create or keep an image, which is per-GB rent for as long as it exists
  • delete: destroy a server, image, volume or firewall
  • dispatch: start a hosted CI run, which consumes a metered minute allowance

What makes a grant a grant

Required fields: scope, budgetEur, expiresAt, issuedAt, issuedBy, nonce, reason. Single use, at most 24 hours, written to /var/lib/jarvisos/secrets/burst-grants at mode 0600.

  • An expired grant is not a grant.
  • A replayed nonce is not a grant.
  • A grant whose ceiling exceeds the policy's per-run ceiling is not a grant.
  • A grant that does not name the operation is not a grant.
  • A refused operation is refused. It is never retried with a stronger credential.

One grant covers a create and its own teardown. The reaper may delete only resources labelled with that grant's nonce, and may delete nothing else — otherwise a lease that has to wake a human to be enforced is not a lease.

Operation Call Effect Billable Approval Credential
hz-pricing The provider's own price list. Every euro in this register is arithmetic on a number that this call is how you check. GET /v1/pricing read free auto hetzner-read not resolvable
hz-server-types Available server types with their per-hour and per-month prices, used to resolve a workload profile to a concrete type. GET /v1/server_types read free auto hetzner-read not resolvable
hz-servers-list Inventory. What exists in the burst project right now, with labels. The call the reaper decides on and the call that answers 'is anything running that nobody remembers'. GET /v1/servers read free auto hetzner-read not resolvable
hz-server-get Health of one burst node: power state, creation time and therefore its age against the lease. GET /v1/servers/{id} read free auto hetzner-read not resolvable
hz-server-metrics CPU, disk and network series for a running burst node. Read-only and free, so a stuck job is visible without touching it. GET /v1/servers/{id}/metrics read free auto hetzner-read not resolvable
hz-images-list Which snapshots exist and how large they are, which is the same question as how much per-GB rent is being paid this month. GET /v1/images read free auto hetzner-read not resolvable
hz-firewalls-list The deny-all-inbound firewall every burst node is created with. Read so the console can say whether it still exists before anything is provisioned into it. GET /v1/firewalls read free auto hetzner-read not resolvable
hz-server-create Create a burst node: server type from the workload profile, no primary IPv4, deny-all-inbound firewall, cloud-init that joins the tailnet with an ephemeral pre-authorised key, and labels carrying the grant nonce and the lease deadline. POST /v1/servers create bills owner hetzner-write not resolvable
hz-server-delete Destroy a burst node. The only teardown that stops the invoice, and therefore the only teardown this contract recognises. DELETE /v1/servers/{id} destroy bills owner hetzner-write not resolvable
hz-server-poweroff Power a burst node off without deleting it. Present in the adapter so the contract can say what it is for, which is debugging a failed run before destroying the evidence. THIS DOES NOT STOP BILLING. A powered-off server bills at the full rate until it is deleted. It is not teardown and this register refuses to let any target declare it as such. POST /v1/servers/{id}/actions/poweroff mutate bills owner hetzner-write not resolvable
hz-server-resize Change a server type, in either direction. Never used on a burst node — a burst node is destroyed and recreated — and present only because resizing VIN is the rejected option this register is compared against. Reboots the server. On VIN that is a TLS and forward-auth outage for every published hostname, and upgrade_disk is irreversible: a disk that grew cannot shrink back to the smaller tier. POST /v1/servers/{id}/actions/change_type mutate bills owner hetzner-write not resolvable
hz-server-rebuild Replace a running server's image. Not billable — it changes no tier — and still owner-gated, because destroying a filesystem is destructive whether or not it costs anything. Erases the root filesystem. This is the operation that proves billable and dangerous are two different axes. POST /v1/servers/{id}/actions/rebuild mutate free owner hetzner-write not resolvable
hz-image-create Snapshot a burst node to make the next boot faster. Creates a recurring per-GB charge that lasts until somebody deletes it. The only operation here that converts variable cost into recurring cost. Every snapshot carries an expiry label and the reaper enforces it. POST /v1/servers/{id}/actions/create_image create bills owner hetzner-write not resolvable
hz-image-delete Delete a snapshot and stop paying rent on it. DELETE /v1/images/{id} destroy free owner hetzner-write not resolvable
gh-runs-list Which dispatched CI runs exist and what happened to them. GET /repos/{owner}/{repo}/actions/runs read free auto github-read not resolvable
gh-billing-actions Minutes consumed against the free allowance this month. The automatic read that watches the ceiling, so the first sign of running over is not an invoice. GET /users/{user}/settings/billing/actions read free auto github-read not resolvable
gh-workflow-dispatch Start a hosted CI run. Free inside the monthly allowance and roughly ten times the CPX41 hourly rate outside it, so it is metered work and therefore owner-gated. POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches create bills owner github-dispatch not resolvable

Transient workload classes

Zero recurring cost is the default and the rollback plan. Every workload class must have at least one free eligible target, so that switching the elastic options off is one field in this file rather than a migration. Paid capacity is allowed only where a measured operational problem is recorded next to the spend, in paidBecause and paidEvidence.

Coding agent worker

100.0 h/month · secret · ephemeral

A `claude -p` worker dispatched by the control plane against one repository. Memory-hungry, CPU-light, and the largest single consumer of VIN's memory today.

2 vCPU · 2 GB · 10 GB · 25 min × 120/month × 2 concurrent

Eligible target Fits Per run Per month Start p95 Score
Home1 scratch worker preferred yes €0.00 €0.00 20s 3.30
Hetzner CPX41 burst node yes €0.02 €4.33 75s 2.75

Measured at 366 and 337 MiB resident on VIN on 2026-08-16, about 40% of that node's total memory. The placement plan already moves this class to home1, and it needs a repository checkout and a model API key — material that should not be created and destroyed inside a rented block device.

Costs nothing at this utilisation, so nothing here needs authorising.

x86 container image build

8.0 h/month · internal · ephemeral

Building and pushing an image the estate will actually run. Bursty CPU, large disk churn, no durable state of its own.

4 vCPU · 8 GB · 40 GB · 12 min × 40/month

Eligible target Fits Per run Per month Start p95 Score
Home1 scratch worker preferred yes €0.00 €0.00 20s 3.30
Hetzner CPX41 burst node yes < €0.01 €0.35 75s 3.05
GitHub Actions hosted runner too small €0.00 €0.00 2m 2.75

Eight hours a month. It fits inside home1 comfortably and the images are pushed onto the tailnet anyway, so building them there avoids a WAN round trip of the whole layer set. CPX41 is the overflow when home1 is busy or down; the ARM node is deliberately not eligible because the artefact is x86.

Costs nothing at this utilisation, so nothing here needs authorising.

Repeatable benchmark run

4.0 h/month · public · ephemeral

A timing run whose whole value is that the numbers are comparable between runs. Needs cores nobody else is using for the length of the run.

8 vCPU · 16 GB · 40 GB · 30 min × 8/month

Eligible target Fits Per run Per month Start p95 Score
Home1 scratch worker yes €0.00 €0.00 20s 3.30
Hetzner CPX41 burst node preferred yes €0.02 €0.17 75s 3.05

The one class in this register where paid capacity removes a measured operational problem. It is also the cheapest line in it.

Paid capacity, and why it is allowed here
A benchmark sharing cores with the agent fleet, the control plane and the databases measures the noise, not the change. Home1 is the estate's default compute node precisely because it is busy; four hours a month of a machine with nothing else on it is the only way these numbers mean anything. The alternative interpretation — that the numbers do not need to be comparable — makes the workload pointless rather than cheap.
Evidence: VIN's own load average of 0.04 alongside 527 MiB of swap in use is the shape of the problem: contention on this estate is invisible in CPU and lethal in memory, so a benchmark on a shared node is unreproducible in a way that does not show up in the load average. Cost of the fix at the declared rate: 4 h/month at EUR 0.0433/h, about EUR 0.17/month, against EUR 10.59/month for the permanent upgrade that would still be contended and would still only be 4 vCPU.

Repository test suite

20.0 h/month · internal · ephemeral

npm test and npm run check on every push. Short, frequent, and entirely self-contained: it needs a checkout and Node, and nothing from the estate.

2 vCPU · 4 GB · 10 GB · 6 min × 200/month

Eligible target Fits Per run Per month Start p95 Score
Home1 scratch worker yes €0.00 €0.00 20s 3.30
Hetzner CAX31 burst node (ARM) yes < €0.01 €0.26 75s 3.05
GitHub Actions hosted runner preferred yes €0.00 €0.00 2m 2.75

Twenty hours a month, which is 1200 minutes against a 2000-minute free allowance. It needs no tailnet access at all — this repository has zero runtime dependencies and the suite is pure Node — so it is the one class that can safely run on hardware the estate does not control. Home1 is the fallback the moment the allowance is short, and the ARM node is eligible because the test suite is architecture-portable even though the images are not.

Costs nothing at this utilisation, so nothing here needs authorising.

Offsite restore rehearsal

1.5 h/month · secret · ephemeral

Pull the encrypted offsite copy onto a machine that has never seen it, decrypt it, and prove it restores. Large disk, long run, once a month.

2 vCPU · 4 GB · 200 GB · 90 min × 1/month

Eligible target Fits Per run Per month Start p95 Score
Home1 scratch worker yes €0.00 €0.00 20s 3.30
Hetzner CPX41 burst node preferred yes €0.06 €0.06 75s 3.05

Eligible on home1 as the free fallback, and the fallback is genuinely worse rather than merely cheaper — which is what a fallback is supposed to be.

Paid capacity, and why it is allowed here
A restore drill run on home1 rehearses the case where home1 still exists, which is the case that does not need the offsite copy. Restoring onto a machine in a different failure domain, from nothing but the bucket and the passphrase, is the only version of this drill that proves anything. One and a half hours a month.
Evidence: config/placement.json failure domains: home1 and the NAS are both fd-home-rack, and the offsite copy in fd-offsite exists specifically to survive that domain. A drill inside the domain it is protecting against is a test with the failure removed. Cost at the declared rate: 1.5 h/month at EUR 0.0433/h, about EUR 0.07/month.

Lifecycle of one burst run

  1. select auto
    Resolve the workload class to a target: the highest weighted score among eligible targets that fit the profile, free targets first at equal score.
    Pure arithmetic on this file. Costs nothing, changes nothing, and is the step that usually ends with 'home1'.
  2. estimate auto hz-pricing
    Derive the per-run cost and the month-to-date spend against the ceiling.
    Reads the provider's price list so the estimate is against today's prices rather than the ones written in this file.
  3. authorise owner
    The owner issues a grant: scope, ceiling, expiry, nonce, reason. JarvisOS cannot issue one to itself.
    The gate. Everything above this line is free and reversible; everything below it is neither.
  4. provision owner hz-server-create
    Create the node with no public IPv4, a deny-all-inbound firewall, and labels carrying the grant nonce and the lease deadline.
    Bounded by: The grant's ceiling and the target's monthlyCeilingEur, whichever is lower.
  5. attach owner
    cloud-init installs tailscaled and joins the tailnet with an ephemeral, tagged, pre-authorised key that expires with the node.
    Part of the create call's user_data, so it is the same authorisation. The key is ephemeral and tagged tag:burst: an ACL grants tag:burst only the ports a job needs, and a node that dies leaves no tailnet record behind.
  6. verify auto hz-server-get
    Confirm the node is up, reachable over the tailnet and inside its lease before any work is sent to it.
  7. run auto
    Dispatch the job over the tailnet using the existing remote dispatch contract in config/remote.json.
    Deliberately the same dispatch path as home1. A burst node is just another node with a shorter life; inventing a second way to run work on it would double the surface that has to be trusted.
  8. collect auto
    Pull the evidence back before teardown. Anything not collected is gone, because the disk is about to be deleted.
  9. destroy owner hz-server-delete
    Delete the node. Not stop, not power off — delete.
    Bounded by: Servers labelled with the nonce of the grant that created them, and nothing else.
  10. reap owner hz-server-delete
    Sweep for burst resources past their lease deadline and delete them, then sweep for snapshots past their expiry and delete those.
    Bounded by: Same nonce restriction. Carried by the grant that created each resource, which is what lets an unattended timer enforce a lease without holding a standing authority to delete.
    Runs on VIN, never on a burst node. A machine cannot be trusted to delete itself, and the case the reaper exists for is precisely the one where the burst node is wedged.

Credentials

Provider credentials are never in this file, in any config, in argv, in a log line, in a report or in browser storage. Each is declared by name and location only and resolved at call time from an environment variable or a 0600 file under /var/lib/jarvisos/secrets, outside this working tree and owned by the jarvis service account.

Credential Variable Protected location Ability Rotation Present
hetzner-readRead-only inventory, health, pricing and image listing in the burst-only project. The credential every automatic operation uses. HCLOUD_TOKEN_READ /var/lib/jarvisos/secrets/hetzner-read.token mode 0600 read-only 180 d absent
hetzner-writeCreate and destroy burst nodes and their snapshots. The credential the owner authorisation policy exists to gate. HCLOUD_TOKEN_WRITE /var/lib/jarvisos/secrets/hetzner-write.token mode 0600 can spend 90 d absent
github-readRead dispatched run status and the Actions minute allowance. GITHUB_TOKEN_BURST_READ /var/lib/jarvisos/secrets/github-burst-read.token mode 0600 read-only 180 d absent
github-dispatchStart a hosted CI run, spending metered minutes. GITHUB_TOKEN_BURST_DISPATCH /var/lib/jarvisos/secrets/github-burst-dispatch.token mode 0600 can spend 90 d absent

Presence only. Nothing on this page has opened one of these files or read one of these variables, and no value from either appears in this document, in /api/v1/burst or in any log line.

Rollback and recovery

If this happens Do this Who Reversible Time
The elastic experiment is abandoned Set decision to 'reject' on hetzner-cpx41, hetzner-cax31 and github-actions, and set every workload's preferredTarget to its free eligible target. Revoke both Hetzner tokens in the provider console and delete the burst project. npm run check enforces that a free fallback existed for every class, so this is an edit rather than a migration. operator yes minutes
A burst node is wedged and the reaper did not delete it node scripts/jarvisos-burst.mjs reap --confirm, or delete the server in the Hetzner console. The monthly cap bounds the damage in the meantime: worst case is the target's cap, not the hourly rate multiplied by however long nobody looked. operator yes under an hour, bounded by the cap regardless
The write credential leaks Revoke the token in the Hetzner console. Because it is scoped to the burst-only project, the blast radius is burst nodes and their snapshots — VIN, its volumes and its snapshots are in a different project and are not reachable with it. Delete the burst project outright if in doubt. owner yes minutes, and this project separation is the reason it is minutes
A grant is issued in error Delete the grant file from /var/lib/jarvisos/secrets/burst-grants/. It is single-use and expiring; an unspent grant that is deleted was never spent. If it was already spent, the resource it created carries its nonce and is deletable by that label. owner yes immediate
The permanent upgrade turns out to have been right after all The register says so before the invoice does: when projected hours cross the derived break-even, the console raises burst-exceeds-breakeven and names the permanent option and its price. Resize VIN with hz-server-resize, accepting the reboot of the public edge, or provision a second permanent node so the edge is never rebooted for a capacity decision. owner no a resize is a reboot; a disk that grew cannot shrink back
Tailscale attach fails on a new burst node The node is unreachable and must be destroyed rather than debugged: it has no public IPv4 and no inbound firewall rule, which is the design working. Debug by provisioning with a serial console session in the provider UI, never by adding an address. operator yes minutes

Gaps

Notes

How to read this page

Source: config/burst.json · machine-readable at /api/v1/burst · narrative in docs/BURST-COMPUTE.md · the placement decision this sits on top of is at the placement plan and docs/HOSTING-TOPOLOGY.md.