{"record":{"id":"48b5a96b6567008e","repo":"juanfont/headscale","slug":"failed-to-allocate-ip","errorCode":null,"errorMessage":"failed to allocate IP","messagePattern":"failed to allocate IP","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/ip.go","lineNumber":164,"sourceCode":"\n\tif i.prefix4 != nil {\n\t\tret4, err = i.allocateNext(&i.prev4, i.prefix4)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"allocating IPv4 address: %w\", err)\n\t\t}\n\t}\n\n\tif i.prefix6 != nil {\n\t\tret6, err = i.allocateNext(&i.prev6, i.prefix6)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"allocating IPv6 address: %w\", err)\n\t\t}\n\t}\n\n\treturn ret4, ret6, nil\n}\n\nvar ErrCouldNotAllocateIP = errors.New(\"failed to allocate IP\")\n\n// allocateNext allocates the next address from prefix under i.mu, advancing\n// prev so a run of allocations (e.g. BackfillNodeIPs) does not rescan\n// already-issued addresses, and so prev is read under the lock rather than in\n// the caller's frame.\nfunc (i *IPAllocator) allocateNext(prev *netip.Addr, prefix *netip.Prefix) (*netip.Addr, error) {\n\ti.mu.Lock()\n\tdefer i.mu.Unlock()\n\n\tret, err := i.next(*prev, prefix)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t*prev = *ret\n\n\treturn ret, nil\n}","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/ip.go#L146-L182","documentation":"Exported sentinel from hscontrol/db/ip.go returned by IPAllocator.allocateNext (ip.go:227, ip.go:238) when the address pool is exhausted: sequential allocation walks past the end of the prefix, random allocation wraps and returns to its starting address. Every candidate was either already used, reserved for Tailscale infrastructure, or outside the prefix. It surfaces from SaveNodeIP-style paths (db/node.go:672) when registering a node.","triggerScenarios":"prefixes.v4 (e.g. 100.64.0.0/10) or prefixes.v6 fully allocated — registering a new node finds no free address. Also triggered in unit tests with a /30 prefix after all addresses are handed out (see db/ip_random_exhaustion_test.go).","commonSituations":"Using a too-small prefix such as 100.64.0.0/24 or /28 for a large tailnet; address leaks from many ephemeral test nodes never expired; misconfigured prefixes after initial setup.","solutions":["Enlarge ip_prefixes in config.yaml (e.g. 100.64.0.0/10, the Tailscale default) and restart headscale","Delete expired/ephemeral nodes holding addresses (`headscale nodes list`, then `headscale nodes delete`)","Verify prefix size with a quick count of allocated IPs vs prefix capacity before scaling the tailnet"],"exampleFix":"# before\nprefixes:\n  v4: 100.64.0.0/24\n\n# after\nprefixes:\n  v4: 100.64.0.0/10","handlingStrategy":"validation","validationCode":"// capacity check before mass-enrolling nodes\nfunc capacityLeft(prefix netip.Prefix, used int) int {\n    total := 1 << (prefix.Bits() // host bits)\n    usable := total - reservedCount(prefix) // network/bcast + tailscale-reserved\n    return usable - used\n}","typeGuard":null,"tryCatchPattern":"node, _, err := hsdb.SaveNodeIP(...)\nif err != nil {\n    if errors.Is(err, db.ErrCouldNotAllocateIP) {\n        // not retryable at the same prefix: widen prefixes or prune nodes\n        return fmt.Errorf(\"IP pool exhausted; enlarge ip_prefixes or delete unused nodes: %w\", err)\n    }\n    return err\n}","preventionTips":["Size prefixes.v4 with headroom (100.64.0.0/10 supports the full tailnet range)","Expire/delete ephemeral test nodes so addresses return to the pool","Alert when allocated-node count approaches prefix capacity"],"tags":["ip-allocation","capacity","config","node-registration","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}