Speed and cost
Warm boxes and reuse
Keep a server for a short window after the work, reuse it for the next run, and know exactly when it is sunk.
Reviewed 2026-10-04
Most servers run a single flow and are thrown away, and the next flow pays the
full boot again. A config that declares session.keepFor
can instead release the server into a warm window and reuse it. Warm boxes are
Beta, are for servers in your own cloud account, and need a healthy local
teardown schedule so something sinks the server when the window ends.
Release a box into a warm window
chalupa session release --config ./chalupa.yml # keep warm for session.keepFor
chalupa session release --config ./chalupa.yml --keep-for 30m
chalupa session release --config ./chalupa.yml --sink # the confirmed chalupa down
A release moves the registered deadline to now plus the keep window and, once that is recorded, stops the supervised tunnel so the idle clock starts. A refused release leaves the tunnel running. It can only shorten a lease, so it never adds spend and needs no confirmation phrase.
The console receives a signed released lifecycle event and shows the
environment as warm · kept until HH:MM; chalupa status shows the same line.
On Hetzner, which bills each started hour, the window runs to the end of the
hour already paid, never past the existing deadline, and the maximum cost is
shown for that hour.
Releasing promises that something will sink the box when the window ends, so
session release checks that first: the local teardown schedule must be
installed and healthy (chalupa teardown schedule:doctor; see
Expiry and the local teardown runner). The console
reaper only sweeps organizations that stored a provider credential, and the CLI
cannot see that, so it is not accepted as proof. Without a healthy schedule the
release refuses, changes nothing, and says to install the schedule or sink with
--sink. chalupa up warns about a missing schedule for a warm config before
it launches. chalupa test releases instead of sinking only when keepFor is
declared; if the release fails or is refused it sinks.
Reuse a warm box with chalupa up
With session.reuse: auto (the default once keepFor is declared),
chalupa up first checks whether the box that is already there still matches
this config. Three things must hold: the local compute stack exists, the
provider reports the recorded server running, and the teardown registration
pins the same config digest.
When all three hold, chalupa up launches nothing, needs no launch phrase,
re-leases the session from now (never past session.maxLifetimeHours),
re-arms the supervised tunnel (unless tunnel.onReuse: none) and prints
reusing <environment> · up 37m · kept until 15:10. When the config changed it
refuses, names the changed keys, and asks for chalupa up --fresh (FRESH=1
for the task). Anything it cannot prove, such as no provider token or no
registration, takes the normal launch path with its normal confirmation.
Reuse needs no phrase, but it is not free: it re-leases a released box up to
session.expiresAfterMinutes from now, which is billable time like
session extend. The reuse line prints the ceiling (re-leased, max +$0.06).
Without a terminal, chalupa up still needs --config PATH --confirm "launch <env>"
even when it would only reuse a warm box: the terminal rule runs before the
reuse check. With the phrase, an unchanged live box is reused and a changed
config is refused unless --fresh.
Reuse only, never launch
An unattended caller that must never start new compute passes --reuse-only:
chalupa up --reuse-only --config ./chalupa.yml
It reuses the live, unchanged box exactly as above (re-lease, tunnel, one-line
summary), and needs neither a terminal nor a phrase because it can never launch
or bill new compute. Whenever the guard would have taken the launch path it
refuses instead: exit 1, nothing launched, and the first words of the message
are a reason code, for example reuse-only refused (provider-unknown): ....
--reuse-only requires --config on the same command line, cannot be combined
with --fresh or --confirm, and is for servers in your own cloud account.
| Reason code | Meaning |
|---|---|
reuse-never |
The config has no session.keepFor and no session.reuse: auto. |
no-stack |
No local compute stack exists. |
no-provider-id |
The stack has no recorded provider ID. |
provider-absent |
The provider no longer lists the server. |
provider-unknown |
The provider could not be asked (no token, API unreachable). |
not-running |
The provider reports the server is not running. |
no-registration |
There is no teardown registration. |
other-deployment |
The registration belongs to a different deployment. |
config-changed |
The config differs from the registered one; the message names the changed keys. |
expired |
The session deadline has passed and the runner is about to sink the box. Renew it with chalupa session renew, or launch fresh. |
guard-unavailable |
The guard itself could not be read. |
What counts as a changed config
Edits confined to the ci: section do not count. The box is built from
everything outside ci: (stack, image, size, volumes, network, session); the
suite, steps, secrets, budget and the rest of ci: reach it over SSH when you
arm, so changing ci.suite between two runs on a warm box is the point of
keeping it, not a reason to boot cold. The reuse line says which ci.*
settings changed and re-pins the registration to the edited file, so the finish
record and the next reuse still match.
ci.onFinish is the exception: the local teardown runner reads it from the
registered file to decide whether the box is sunk when a run ends, so changing
it (other than between keep and leaving it unset) still refuses and needs
--fresh. So does any ci edit made together with a change outside ci:. A
registration made before key digests existed cannot prove what changed and
keeps refusing.
A reused box still holds the earlier runs' files, and each run reads only its
own. A uses: cairntrace step writes to <artifactRoot>/<runId>/<stepId>, so
its evidence pack and its cairn stats report never pick up an earlier run's;
ci follow, ci collect and ci report know which run was armed last and
never present an earlier one as it. See
CI.
Reuse and the tunnel
A reuse re-opens the supervised tunnel, which binds every forwarded Compose
port on your machine. An orchestrator that opens its own forward to one of those
ports sets tunnel.onReuse: none so the reuse leaves the tunnel closed. The
cold counterpart is tunnel.afterUp: none. Both are explained in
Supervised tunnels.
Limits to plan for
- Reuse does not reset service data. The box matches the config only in shape;
whatever the previous flow wrote to the database or volumes is still there.
Make flows namespace-isolated, or have them call their own reset or seed
task. A
cleanupRecipesentry is the reviewed way to reset between runs. - Only
chalupa.ymlis pinned. Edits to the Compose file it references, or to an image behind a tag, are not detected. Runchalupa up --freshafter changing them. - A warm box bills until the window ends. The maximum is shown at the launch confirmation and in the reuse line.