Hmbown/CodeWhale · error
Cloud agent sandbox teardown failed
Error message
Cloud agent sandbox teardown failed (HTTP {status}). What it means
Thrown by `teardown` when the control-plane `DELETE sandbox/{id}` returns neither a success status nor 404. 2xx and 404 are treated as idempotent success (the comment notes Daytona returns 204 on delete and 404 means already gone); any other status — typically 401/403/429/5xx — means the sandbox was not confirmed deleted. The sandbox may therefore still be running and incurring provider cost.
Solutions
- Retry the teardown — DELETE is idempotent here, so re-invoking later is safe and will 404 once gone.
- Check the HTTP status: 401/403 means fix CODEWHALE cloud credentials (`Self::api_key`) before retrying; 429 means back off and retry.
- If it keeps failing, delete the sandbox by id in the provider dashboard to stop billing, then retry teardown to reconcile local state.
- Audit for orphaned sandboxes from prior failed teardowns via `list_job_sandboxes` and clean them up.
Example fix
// caller: teardown is idempotent, so retry with backoff
for attempt in 0..3 {
match provider.teardown(&receipt) {
Ok(()) => break,
Err(e) if attempt < 2 => std::thread::sleep(Duration::from_secs(2 * (attempt + 1))),
Err(e) => return Err(e),
}
} Defensive patterns
Strategy: retry
Try / catch
// DELETE is idempotent (2xx or 404 both succeed), so retry with backoff
for attempt in 0..3 {
match provider.teardown(&receipt) {
Ok(()) => break,
Err(e) if attempt == 2 => {
log_orphan_sandbox(&receipt.sandbox_id); // clean up manually
}
Err(_) => std::thread::sleep(Duration::from_secs(2u64.pow(attempt))),
}
} Prevention
- Always retry failed teardowns — 2xx and 404 are both success, so retries are safe.
- Run a periodic reconciliation pass via list_job_sandboxes to reap orphans.
- Rotate API keys on a schedule so teardown-time 401s are rare.
- Back off on 429 instead of hammering DELETE during mass cancellations.
When it happens
Trigger: Canceling or completing a cloud job when the DELETE call fails: expired/invalid API key (401), rate limiting (429), control-plane outage (5xx), or a conflict status if the provider disallows deleting a sandbox in its current state.
Common situations: API key rotated or revoked between job start and teardown; provider rate limits hit during mass job cancellation; transient Daytona outage leaving many orphaned sandboxes.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Cloud agent create failed
- Cloud agent harness execution failed
- cloud agent label apply failed
- Cloud agent repository clone failed
- Cloud agent sandbox listing failed
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/98d5e3ece5319c16.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/cloud_dispatch.rs:1780
fn teardown(&self, receipt: &SandboxReceipt) -> Result<()> {
let api_key = Self::api_key()?;
if !valid_sandbox_id(&receipt.sandbox_id) {
bail!("the sandbox id is not a usable path token");
}
let url = Self::control_plane_url(&format!("sandbox/{}", receipt.sandbox_id))?;
let response = Self::send_json(
reqwest::Method::DELETE,
&url,
&api_key,
serde_json::Value::Null,
)?;
// Daytona returns 204 on delete; treat 404 as already-gone success so
// cancel/complete teardown is idempotent.
let status = response.status();
if status.is_success() || status.as_u16() == 404 {
Ok(())
} else {
bail!("Cloud agent sandbox teardown failed (HTTP {status}).");
}
}
fn list_job_sandboxes(&self) -> Result<Vec<LabeledSandbox>> {
let api_key = Self::api_key()?;
// The provider's list call takes a JSON-encoded exact-match labels
// filter (same OpenAPI family as create/get/delete above); filtering
// on the product tag keeps the response to Codewhale dispatch
// sandboxes only — never the user's own sandboxes on a shared key.
let mut url = Self::control_plane_url("sandbox")?;
url.query_pairs_mut().append_pair(
"labels",
&format!("{{\"{SANDBOX_PRODUCT_LABEL}\":\"{SANDBOX_PRODUCT_VALUE}\"}}"),
);
let response = Self::send_json(
reqwest::Method::GET,
&url,
&api_key,View on GitHub (pinned to 73e0f67d83)