In-Place Upgrades
When the template a stack was launched from has been updated to a new version, you can adopt those changes without tearing the stack down. The upgrade engine diffs the running stack's snapshotted descriptor against the latest template version, classifies each resource's delta, and applies only the changes that are safe to make non-destructively.
How the upgrade engine works
The engine follows a three-step process:
- Diff. It compares the snapshotted descriptor stored on your stack at launch time against the current template version. Each resource in the descriptor receives an independent diff.
- Classify. Each resource's diff is classified as
unchanged,in_place, orblockedaccording to the rules below. If any resource isblocked, the entire upgrade is blocked. - Advance. For unblocked upgrades, the engine advances each resource with an
in_placeclassification through the appropriate platform operation (for example, a compute plan resize or an app redeployment). Resources classified asunchangedare not touched.
How the engine classifies changes
Every resource in the diff receives one of three classifications:
| Classification | Meaning |
|---|---|
unchanged | The resource's spec is identical in both versions. The engine does not touch it. |
in_place | A safe, non-destructive change the engine can apply to the running resource. |
blocked | A change that would recreate, shrink, or fundamentally alter a stateful resource. A fresh stack is required. |
If any resource is blocked, the entire upgrade is blocked. The engine will not apply the in_place changes either. You must launch a new stack to adopt those changes.
What can be upgraded in place
The set of spec keys the engine treats as in_place differs by resource kind:
| Kind | In-place keys | Action label |
|---|---|---|
app | image_ref, env, auth, domain | redeploy_app |
app | plan_name | scale_app |
database | plan_name (compute resize) | scale_service |
inference | (none; any change is blocked in the current version) | n/a |
files | (none; any change is blocked in the current version) | n/a |
Changes to any other spec key on any kind are blocked. The intent is conservative: the engine cannot recreate or shrink a stateful resource (a database or a bucket) in place, so those paths are entirely refused.
What is always blocked
The following changes are always blocked, regardless of kind:
- Adding a resource. A new resource in the target template has no running counterpart. Adding it during an upgrade is not supported; launch a fresh stack.
- Removing a resource. Removing an existing resource would leave no path to tear it down gracefully within the upgrade. Launch a fresh stack.
- Changing a resource's kind. For example, changing a
databaseresource to afilesresource. - Changing a database engine, version, storage size, or storage tier. Any key on a
databaseresource other thanplan_nameis blocked. - Any change to an
inferenceorfilesresource. Support for in-place inference key reminting and files quota adjustment is planned but not yet available.
Step 1: Preview the upgrade
Always preview before applying. The preview computes and returns the classified plan without changing any state:
curl -X POST https://api.foundrydb.com/stacks/$STACK_ID/upgrade/preview \
-H "Authorization: Bearer $FOUNDRYDB_TOKEN"
Example response for an unblocked upgrade:
{
"from_version": "1.0.0",
"to_version": "1.1.0",
"changes": [
{
"symbolic_name": "db",
"kind": "database",
"change": "unchanged"
},
{
"symbolic_name": "app",
"kind": "app",
"change": "in_place",
"action": "redeploy_app",
"target_spec": {
"attachment_kind": "open-webui",
"image_ref": "ghcr.io/open-webui/open-webui:v0.10.2"
}
}
],
"new_monthly_cost": 87.00,
"current_monthly_cost": 87.00,
"cost_delta": 0.0,
"blocked": false,
"blocked_reasons": []
}
Example response for a blocked upgrade:
{
"from_version": "1.0.0",
"to_version": "2.0.0",
"changes": [
{
"symbolic_name": "db",
"kind": "database",
"change": "blocked",
"reason": "changing \"version\" on a database resource requires a fresh stack (it could recreate or resize stateful data)"
},
{
"symbolic_name": "app",
"kind": "app",
"change": "in_place",
"action": "redeploy_app"
}
],
"blocked": true,
"blocked_reasons": [
"db: changing \"version\" on a database resource requires a fresh stack (it could recreate or resize stateful data)"
]
}
When blocked is true, no upgrade path is available. Launch a new stack from the updated template and migrate your data manually.
Step 2: Apply the upgrade
Once you have reviewed the plan and accepted the new monthly cost:
curl -X POST https://api.foundrydb.com/stacks/$STACK_ID/upgrade \
-H "Authorization: Bearer $FOUNDRYDB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"accepted_monthly_cost": 87.00}'
Response semantics:
| HTTP status | Meaning |
|---|---|
202 Accepted | Upgrade accepted. The reconciler is applying changes. Response body is a StackMigration record. |
200 OK with {"status":"up_to_date"} | No changes to apply. The stack is already on the latest template version. |
409 Conflict | An upgrade is already in progress, or the accepted cost drifted from the current estimate by more than $0.01. Re-preview and resubmit. |
422 Unprocessable Entity | The plan is blocked. Launch a fresh stack to adopt these changes. |
Monitoring the upgrade
While the upgrade is in progress, the stack's status moves to Upgrading. The StackMigration record returned by the 202 response tracks the upgrade's lifecycle:
| Migration status | Meaning |
|---|---|
Accepted | Upgrade accepted, reconciler not yet started. |
Applying | The reconciler is applying in-place changes. |
Completed | All changes applied. The stack is back to Running. |
Failed | A single step failed (see below). |
Retrieve the stack to see the current status:
curl https://api.foundrydb.com/stacks/$STACK_ID \
-H "Authorization: Bearer $FOUNDRYDB_TOKEN"
Failure behaviour
A failed upgrade is scoped to the in-flight step only. The engine rolls back that one change and returns the stack to Running. It does not trigger a full stack teardown, does not touch other running resources, and does not delete any data.
The migration record's status moves to Failed and status_detail describes what went wrong (for example, "app redeployment failed: image pull error"). After reviewing the migration record, you can re-preview and reapply once you have resolved the issue, or leave the stack on its current template version and apply the upgrade later.
Because the stack returns to Running after a failed step, your application remains available throughout. The only observable effect is that the attempted in-place change (for example, the new image version) did not take effect.
Cost gate
The upgrade endpoint enforces the same cost gate as the launch endpoint. accepted_monthly_cost must be within $0.01 of the estimate returned by the upgrade preview. If the template's resources have changed price between preview and apply (because, for example, a plan tier changed list price), you receive 409 Conflict and must re-preview with the updated estimate.
This design ensures the upgrade does not silently increase your bill. Re-preview as many times as needed until the estimate is stable; only then submit the apply request.
Deciding between upgrade and a fresh stack
| Situation | Recommendation |
|---|---|
| Only app image updates or compute plan resizes | Use in-place upgrade |
| Template version adds or removes a resource | Launch a fresh stack |
| Database engine, version, storage size, or storage tier changed | Launch a fresh stack |
Upgrade preview returns blocked: true | Launch a fresh stack |
| You want a clean environment before switching traffic | Launch a fresh stack |
| You need zero data migration | Use in-place upgrade |
When launching a fresh stack to adopt blocked changes, provision the new stack first, migrate your data, then delete the old stack. The old and new stacks run independently; you control the cutover timing.