juanfont/headscale · error
saving node(%d) after adding IPs: %w
Error message
saving node(%d) after adding IPs: %w
What it means
After computing new or removed IPs for a node, backfill persists only the ipv4/ipv6 columns via Updates(Select(...)). This error is that UPDATE failing at the DB layer — constraint violation, lock, or connectivity. The comment in code notes Select() is deliberate so removing an IP (writing nil) is not skipped as a zero value.
Source
Thrown at hscontrol/db/ip.go:374
ret = append(ret, fmt.Sprintf("removing IPv4 %q from Node(%d) %q", node.IPv4.String(), node.ID, node.Hostname))
node.IPv4 = nil
changed = true
}
// IPv6 prefix is not set, but node has IP, remove
if i.prefix6 == nil && node.IPv6 != nil {
ret = append(ret, fmt.Sprintf("removing IPv6 %q from Node(%d) %q", node.IPv6.String(), node.ID, node.Hostname))
node.IPv6 = nil
changed = true
}
if changed {
// Use Updates() with Select() to only update IP fields, avoiding overwriting
// other fields like Expiry. We need Select() because Updates() alone skips
// zero values, but we DO want to update IPv4/IPv6 to nil when removing them.
err := tx.Model(node).Select("ipv4", "ipv6").Updates(node).Error
if err != nil {
return fmt.Errorf("saving node(%d) after adding IPs: %w", node.ID, err)
}
}
}
return nil
})
return ret, err
}
func (i *IPAllocator) FreeIPs(ips []netip.Addr) {
i.mu.Lock()
defer i.mu.Unlock()
for _, ip := range ips {
i.usedIPs.Remove(ip)
}
}View on GitHub (pinned to 565fd254d0)
Solutions
- Inspect the wrapped error for the specific constraint or lock message
- Query for duplicate ipv4/ipv6 values across nodes and fix the offending rows
- If lock-related on SQLite, serialize startup backfill away from heavy registration load
- Re-run; backfill re-evaluates each node idempotently
Defensive patterns
Strategy: try-catch
Try / catch
if _, err := db.BackfillNodeIPs(ipAlloc); err != nil {
if isTransientDBError(err) {
// retry at next startup; backfill is idempotent
}
log.Error().Err(err).Msg("backfill save failed")
} Prevention
- Keep a unique index on ipv4/ipv6 to catch duplicates early
- Avoid restoring partial database dumps that desync IP ownership
- Watch the wrapped driver error, not the outer message
When it happens
Trigger: Unique index conflict when another row already holds the allocated IP; SQLite lock timeout; connection loss mid-transaction; a manually inserted row violating assumptions.
Common situations: Two nodes assigned the same IP after a partial restore; concurrent registration racing backfill on SQLite; DB volume full.
Related errors
- backfilling IPs: %w
- listing nodes to backfill IPs: %w
- allocating IPv4 for node(%d): %w
- allocating IPv6 for node(%d): %w
- checking name uniqueness: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/1039e6a60104467c.
Report an issue: GitHub.