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 stale

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Re-list nodes (headscale node list or GET /api/v1/node) to confirm the ID still exists
  2. If the node was deleted, re-register it or remove the stale ID from your automation
  3. Check for concurrent deletions by other admins or cleanup jobs
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/2f0dd8f2aa6d374d. Report an issue: GitHub.