juanfont/headscale · error

setting node expiry: %w

Error message

setting node expiry: %w

What it means

Wraps a failure of h.state.SetNodeExpiry(node.ID(), &expiry) during logout of a non-ephemeral node (hscontrol/auth.go:265). The code clamps the client-supplied expiry (tailscaled sends the 1970 sentinel time.Unix(123,0)) to now, then persists it; if the state/DB update fails, the error is returned and the node remains non-expired.

Source

Thrown at hscontrol/auth.go:265

		return nodeToRegisterResponse(node), nil
	}

	// Update the internal state with the nodes new expiry, meaning it is
	// logged out.
	//
	// Clamp the client-supplied value to now: Tailscale sends the
	// sentinel time.Unix(123, 0) on logout (controlclient/direct.go),
	// and storing it verbatim propagates a 1970 KeyExpiry to every
	// peer's netmap. Semantically identical (expired as of now), but
	// sane in logs, debug dumps, and peer netmaps.
	expiry := req.Expiry
	if now := time.Now(); expiry.Before(now) {
		expiry = now
	}

	updatedNode, c, err := h.state.SetNodeExpiry(node.ID(), &expiry)
	if err != nil {
		return nil, fmt.Errorf("setting node expiry: %w", err)
	}

	h.Change(c)

	return nodeToRegisterResponse(updatedNode), nil
}

// isAuthKey reports if the register request is a registration request
// using an pre auth key.
func isAuthKey(req tailcfg.RegisterRequest) bool {
	return req.Auth != nil && req.Auth.AuthKey != ""
}

func nodeToRegisterResponse(node types.NodeView) *tailcfg.RegisterResponse {
	resp := &tailcfg.RegisterResponse{
		NodeKeyExpired: node.IsExpired(),

		// Headscale does not implement the concept of machine authorization

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Read the wrapped error to identify the DB cause and address it (locks, connectivity).
  2. Retry logout from the node after DB recovery — the expiry update is idempotent.
  3. If the node cannot retry, expire it administratively: `headscale nodes expire -i <id>`.
  4. Reduce write contention (single writer for SQLite, adequate pool for PostgreSQL) if it recurs during node churn.
Defensive patterns

Strategy: retry

Try / catch

resp, err := h.handleRegister(req, mk)
if err != nil && strings.Contains(err.Error(), "setting node expiry") {
    // idempotent operation: retry logout, or run `headscale nodes expire -i <id>`
}

Prevention

When it happens

Trigger: A normal (non-ephemeral) node logs out while the database errors: connection lost, SQLite write lock timeout, or node row updated concurrently by the mapper (optimistic-update conflict).

Common situations: Logout racing a netmap update on the same node; DB restarts; SQLite deployments with concurrent writers.

Related errors


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