juanfont/headscale · error · ErrNodeNotFound
node not found
Error message
node not found
What it means
ErrNodeNotFound is a sentinel error in hscontrol/state/state.go:69 signalling that no node with the requested node ID exists. It is returned wrapped with the ID (e.g. state.go:703 fmt.Errorf("%w: %d", ErrNodeNotFound, id)) from State lookups and from NodeStore.SetGivenName (hscontrol/state/node_store.go:484). API layers map it to HTTP 404 (hscontrol/api/v1/errors.go:23, v2/errors.go:70).
Source
Thrown at hscontrol/state/state.go:69
// TTL and a stripped-down RegistrationData payload (~200 bytes per entry),
// 1024 entries cap the worst-case cache footprint at well under 1 MiB even
// under sustained unauthenticated cache-fill attempts.
defaultRegisterCacheMaxEntries = 1024
// defaultNodeStoreBatchSize is the default number of write operations to batch
// before rebuilding the in-memory node snapshot.
defaultNodeStoreBatchSize = 100
// defaultNodeStoreBatchTimeout is the default maximum time to wait before
// processing a partial batch of node operations.
defaultNodeStoreBatchTimeout = 500 * time.Millisecond
)
// ErrUnsupportedPolicyMode is returned for invalid policy modes. Valid modes are "file" and "db".
var ErrUnsupportedPolicyMode = errors.New("unsupported policy mode")
// ErrNodeNotFound is returned when a node cannot be found by its ID.
var ErrNodeNotFound = errors.New("node not found")
// ErrInvalidNodeView is returned when an invalid node view is provided.
var ErrInvalidNodeView = errors.New("invalid node view provided")
// ErrNodeNotInNodeStore is returned when a node no longer exists in the [NodeStore].
var ErrNodeNotInNodeStore = errors.New("node no longer exists in NodeStore")
// ErrNodeNameNotUnique is returned when a node name is not unique.
var ErrNodeNameNotUnique = errors.New("node name is not unique")
// nodeUpdateColumns lists all Node columns that should be written
// during a struct-based GORM Updates() call. Listing them explicitly
// forces GORM to include nil/zero-value fields (e.g. UserID=nil when
// converting a user-owned node to tagged) that struct-based Updates()
// would otherwise silently skip.
//
// Excluded columns:
// - AuthKeyID, AuthKey: prevents GORM from persisting staleView on GitHub (pinned to 565fd254d0)
Solutions
- Re-list nodes (headscale node list or GET /api/v1/node) to confirm the ID still exists
- If the node was deleted, re-register it or remove the stale ID from your automation
- Check for concurrent deletions by other admins or cleanup jobs
- Handle the 404 in your API client instead of retrying blindly
Example fix
null
Defensive patterns
Strategy: try-catch
Validate before calling
node, err := st.GetNodeByID(ctx, id)
if err != nil { /* check before mutating */ } Type guard
null
Try / catch
if err != nil {
if errors.Is(err, state.ErrNodeNotFound) {
// 404: drop the stale reference, do not retry
}
} Prevention
- Re-check node existence immediately before mutating operations
- In automation, handle 404 as a signal to refresh the node list rather than retrying the same ID
When it happens
Trigger: Calling State methods such as GetNode/UpdateNode with an ID that was never allocated or was deleted; renaming a node via SetGivenName after it was deleted concurrently; gRPC/API requests referencing a stale node ID from an older listing.
Common situations: Node was deleted (via UI or 'headscale node delete') while a client or script still holds its ID; race between listing nodes and operating on one; using IDs from a different headscale instance/database.
Related errors
- node not found
- node name is not unique
- node name is not unique
- route is not available on node
- failed to convert node interface
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/2f0dd8f2aa6d374d.
Report an issue: GitHub.