Skip to content

No DELETE /v1/nodes/:name — node rows are permanent, and one production roster is already at 1225 #353

Description

@khaliqgant

Summary

POST /v1/nodes creates fleet nodes. Nothing deletes them. nodeRoutes exposes DELETE /v1/nodes/:name/agents/:agentName and DELETE /v1/nodes/:node/providers/:name, but there is no DELETE /v1/nodes/:name — so a node row, once created, is permanent for the life of the workspace.

Because POST /v1/nodes is enroll-or-rotate keyed on name and is available to any workspace-key holder, node rows accumulate as a normal consequence of ordinary use: every differently-named CI run, probe, short-lived sandbox, or renamed host leaves a row behind that no API call can remove.

The number is the argument

A production workspace roster currently returns 1225 nodes. The overwhelming majority are status: "offline" with active_agents: 0direct-<snowflake> rows and one-off named nodes from runs that ended long ago.

That is not a cosmetic problem:

  • GET /v1/nodes returns the full roster, so every consumer pays for the dead rows on every read.
  • Operators reading the roster have to distinguish "nodes that exist" from "nodes that matter" by eye, with no supported way to reduce the set.
  • Anything that reasons over the roster inherits the noise.

Concrete instance

While verifying whether a workspace key can enroll a node directly — the answer is yes, and it is the basis of a fix for an unrelated dispatch outage — I created one deliberately throwaway node:

name    factory-selfenroll-probe-20260821-5952a573
tags    ["probe", "factory-selfenroll-feasibility"]
version factory-cloud-selfenroll-probe
status  offline, active_agents 0

It is inert and it will remain in that roster permanently, because there is no call I can make to remove it. Naming it clearly was the only mitigation available. This is a small, honest example of the leak: the write path is one HTTP call and the cleanup path does not exist.

Prior art in this repo

relaycast#336 — "No way to delete a workspace, and CI creates one on every Package Validation run" — is the same shape one level up, and was accepted. The node-level version has the same driver: an automated caller creates a resource on a normal code path and has no way to clean up after itself.

Suggested shape

DELETE /v1/nodes/:name, requireWorkspaceKey, consistent with the existing POST:

  • Refuse (409) while the node is live, so deletion cannot race an active session. isNodeLive already provides the predicate.
  • Refuse (409) while agents are still bound, or require an explicit cascade — the per-agent DELETE already exists for the caller to unwind first.
  • Return 404 for an unknown name, matching the other name-scoped routes.

An offline node with no bound agents is the common case and is safe to remove.

If deletion is deliberately not wanted, a supported alternative would still solve the reading problem: roster filtering (GET /v1/nodes?status=online) or a documented retention policy for offline nodes with no bindings. Any of the three is better than the current state, where the only way a node leaves the roster is that nobody looks at it.

Verified against

origin/main at the time of filing: packages/engine/src/routes/node.ts (route table), packages/engine/src/engine/node.ts (createNodeToken, publicNode), packages/engine/src/engine/placement.ts (isNodeLive, NODE_LIVENESS_TTL_MS).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions