Cancelled/timed-out sandbox create leaves an unregistered orphan instance on the node
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Description
Summary
If a sandbox create request is cancelled or times out on the client side while the node-side create is still in progress, the API reports the create as failed and never registers the sandbox. The node still holds the created instance. It is not in the running store, the team index, or the catalog.
An existing safety net reclaims it: Store.Reconcile → killOrphanSandbox kills instances present on the node but absent from the store, after the orphan grace period (orphanGracePeriod = time.Minute). So the window is bounded, but during that window an unregistered VM is running, and the create failure path itself performs no compensation.
Root cause
The node-side create and the API-side registration are separate steps, and the failure path only cleans up after a successful placement.
In packages/api/internal/orchestrator/create_instance.go, CreateSandbox calls placement.PlaceSandbox. When the request context is cancelled, the placement loop classifies the failure as a timeout and returns before any sandboxStore.Add. The gRPC Create already sent to the node is not cancelled atomically with the API request, so the node completes the create and records the instance, while the API returns an error and never registers it:
ERROR Failed to create sandbox {"sandbox.id":"<id>", "node.id":"<node>", "error":"[Canceled] context canceled"}
WARN error when creating instance "... failed to place sandbox: request timed out ..."
The cleanup that removes a node instance (removeSandboxFromNode) only runs on the sandboxStore.Add failure branch, i.e. after a successful placement; the placement-failure branch does not compensate. The instance is then only reclaimed by the periodic reconcile, one grace period later:
WARN Killing orphaned sandbox not found in store (Store.Reconcile -> killOrphanSandbox)
Reproduction
-
Start a create that takes longer than the client-side timeout (for example a cold/large template on a loaded host), with a short client timeout:
curl -sS --max-time 1 -X POST "$API/sandboxes" \ -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \ -d '{"templateID":"<template>","timeout":3600}' # curl: (28) Operation timed out -
The API logs the cancellation for the corresponding sandbox id (
Failed to create sandbox ... "[Canceled] context canceled"). -
Observe the node holding the instance while it is absent from the running store and team index:
node instance set: [ ..., "<id>" ] # present on the node redis running store: [ ... ] # "<id>" absent team index SET: [ ... ] # "<id>" absent API GET /sandboxes/<id> -> 404 -
Roughly one orphan grace period later, the API logs
Killing orphaned sandbox not found in storeand the instance disappears from the node.
Suggested fix
Compensate on the placement-failure branch instead of relying on the periodic reconcile:
- Record the intended
(sandboxID, executionID)before dispatching the create, and on any failure after the node may have received the create — including cancellation/timeout — issue a best-effortremoveSandboxFromNodefor that exact id. - Alternatively, make node-side create self-cleaning within a bounded window if the caller never confirms registration.
Impact
For up to one orphan grace period, a VM and its network slot are running without any running-store record, any index entry, or any API visibility, and without the create failure path attempting compensation. It is reclaimed by Store.Reconcile, so it is a bounded leak rather than a permanent one.
Related: #2813 (orphaned Firecracker instances / leftover network rules), #3193 (node has no independent sandbox timeout enforcement), #1498 (failed startup leaves a sandbox running).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/api/internal/orchestrator/create_instance.go at CreateSandbox and trace placement.PlaceSandbox's cancellation and failure branches. Use the curl reproduction to observe the node instance, running store, team index, and Store.Reconcile → killOrphanSandbox behavior. Done means placement failures, including cancellation and timeout, attempt best-effort removal for the intended sandbox and do not leave the instance waiting for reconcile.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, infrastructure
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100