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 authorizationView on GitHub (pinned to 565fd254d0)
Solutions
- Read the wrapped error to identify the DB cause and address it (locks, connectivity).
- Retry logout from the node after DB recovery — the expiry update is idempotent.
- If the node cannot retry, expire it administratively: `headscale nodes expire -i <id>`.
- 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
- Treat expiry updates as idempotent retries, never as data loss.
- Use the CLI expire command as an operator fallback when client retries fail.
- Avoid DB maintenance overlapping logout storms.
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
- handling logout: %w
- handling existing node: %w
- deleting ephemeral node: %w
- handling register with auth key: %w
- handling register interactive: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/c98cb52319262f39.
Report an issue: GitHub.