{"record":{"id":"52a304ccbee406db","repo":"juanfont/headscale","slug":"deleting-ephemeral-node-w","errorCode":null,"errorMessage":"deleting ephemeral node: %w","messagePattern":"deleting ephemeral node: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/auth.go","lineNumber":220,"sourceCode":"\n\t// If the request expiry is in the past, we consider it a logout.\n\t// Zero expiry is handled in [Headscale.handleRegister] before calling this function.\n\tif req.Expiry.Before(time.Now()) {\n\t\tlog.Debug().\n\t\t\tEmbedObject(node).\n\t\t\tBool(\"is_ephemeral\", node.IsEphemeral()).\n\t\t\tBool(\"has_authkey\", node.AuthKey().Valid()).\n\t\t\tTime(\"req.expiry\", req.Expiry).\n\t\t\tMsg(\"Processing logout request with past expiry\")\n\n\t\tif node.IsEphemeral() {\n\t\t\tlog.Info().\n\t\t\t\tEmbedObject(node).\n\t\t\t\tMsg(\"Deleting ephemeral node during logout\")\n\n\t\t\tc, err := h.state.DeleteNode(node)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"deleting ephemeral node: %w\", err)\n\t\t\t}\n\n\t\t\th.Change(c)\n\n\t\t\treturn &tailcfg.RegisterResponse{\n\t\t\t\tNodeKeyExpired:    true,\n\t\t\t\tMachineAuthorized: false,\n\t\t\t}, nil\n\t\t}\n\n\t\tlog.Debug().\n\t\t\tEmbedObject(node).\n\t\t\tMsg(\"Node is not ephemeral, setting expiry instead of deleting\")\n\t}\n\n\t// Tagged nodes have key expiry permanently disabled (they are owned by\n\t// their tags, not a user, and never expire - KB 1068). Logging one out has\n\t// no expiry semantics, so do not stamp an expiry on it: doing so leaves the","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/auth.go#L202-L238","documentation":"Wraps a failure of h.state.DeleteNode while removing an ephemeral node during logout (hscontrol/auth.go:220). Ephemeral nodes (registered with an ephemeral pre-auth key) are deleted from the state layer and database on logout; any DB or consistency error — foreign key constraints from related records, lock timeouts, connection failures — is surfaced with this message and the logout response is not returned.","triggerScenarios":"An ephemeral node (ephemeral=true authkey) logs out while the DB is under write load; deletion trips FK constraints on related rows (routes, pre-auth-key usages); SQLite 'database is locked' during concurrent mapper updates; PostgreSQL unreachable mid-transaction.","commonSituations":"CI/ephemeral CI runners deregistering en masse at job end; autoscaling groups terminating ephemeral nodes simultaneously; DB maintenance overlapping with node churn.","solutions":["Check the wrapped DB error in logs; constraint violations indicate related rows not cascading — upgrade headscale (deletion order fixes land periodically).","For lock contention, reduce simultaneous logouts (stagger termination) or switch SQLite deployments to PostgreSQL.","Retry: the node can re-attempt logout; if the node is gone anyway, an admin can remove it manually via `headscale nodes delete -i <id>`.","If persistent, dump logs plus schema version and report upstream."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := h.handleRegister(req, mk)\nif err != nil && strings.Contains(err.Error(), \"deleting ephemeral node\") {\n    // teardown failed; node stays registered — retry logout or delete via `headscale nodes delete`\n}","preventionTips":["Stagger bulk termination of ephemeral nodes (CI shutdown hooks).","Prefer PostgreSQL for high ephemeral churn.","Periodically reconcile with `headscale nodes list` to catch half-deleted ephemeral nodes."],"tags":["ephemeral-nodes","logout","database","registration"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}