cilium/cilium · error

cidr %s has already been allocated

Error message

cidr %s has already been allocated

What it means

occupyCIDR walks the pool's CIDRAllocators, finds the one whose range contains the given prefix, and refuses to occupy it if IsAllocated reports the prefix already claimed. This guards the multi-pool allocator against double-allocation of a pod CIDR within a pool.

Source

Thrown at operator/pkg/ipam/allocator/multipool/pool.go:41

		}

		return alloc.AllocateNext()
	}

	return netip.Prefix{}, errPoolEmpty
}

func occupyCIDR(allocators []cidralloc.CIDRAllocator, cidr netip.Prefix) error {
	for _, alloc := range allocators {
		if !alloc.InRange(cidr) {
			continue
		}
		allocated, err := alloc.IsAllocated(cidr)
		if err != nil {
			return err
		}
		if allocated {
			return fmt.Errorf("cidr %s has already been allocated", cidr)
		}

		return alloc.Occupy(cidr)
	}

	return fmt.Errorf("cidr %s is not part of the requested pool", cidr)
}

func releaseCIDR(allocators []cidralloc.CIDRAllocator, cidr netip.Prefix) error {
	for _, alloc := range allocators {
		if !alloc.InRange(cidr) {
			continue
		}

		allocated, err := alloc.IsAllocated(cidr)
		if err != nil {
			return err
		}

View on GitHub (pinned to ac7b90affa)

Solutions

  1. Determine which node currently holds the CIDR (check CiliumNode Status.IPAM / pool allocations) and reconcile the stale node instead of re-occupying.
  2. Ensure only one operator instance runs (leader election enabled) — duplicate allocators cause exactly this double-occupy.
  3. Delete/restart the stale CiliumNode object so the agent re-requests a fresh CIDR from the current pool state.
  4. If it comes from unorphanCIDR, verify the orphaned CIDR is still actually used by that node (check the node's PodCIDRs/pools) before marking it allocated.

Example fix

// before: blindly occupy on node upsert
if err := p.occupyCIDR(node.Name, pool, cidr); err != nil { ... }
// after: check current owner first
if alloc, err := alloc.IsAllocated(cidr); err == nil && alloc {
	// skip: CIDR already owned; let node reconcile release or re-request
	continue
}
return p.occupyCIDR(node.Name, pool, cidr)
Defensive patterns

Strategy: validation

Validate before calling

// before occupying, check current ownership
occupied, err := alloc.IsAllocated(cidr)
if err != nil { return err }
if occupied {
	// resolve owner first; do not force occupy
	return fmt.Errorf("cidr %s owned by another node", cidr)
}

Type guard

func cidrInPool(allocators []cidralloc.CIDRAllocator, cidr netip.Prefix) bool {
	for _, a := range allocators {
		if a.InRange(cidr) { return true }
	}
	return false
}

Try / catch

if err := p.OccupyCIDR(node, pool, cidr); err != nil {
	if strings.Contains(err.Error(), "has already been allocated") {
		// reconcile stale node instead of retrying occupy
		releaseStaleNodeAllocations(node)
		return nil
	}
	return err
}

Prevention

When it happens

Trigger: PoolAllocator.occupyCIDR(node, pool, cidr) is called with a prefix that is inside a pool allocator's range but already occupied — e.g. unorphanCIDR reclaiming an orphaned CIDR that another node has since been allocated, or a CiliumNode re-registration racing a fresh allocation of the same CIDR.

Common situations: Stale node state: a deleted node's CIDRs were released and re-allocated to another node, then the old node's status (still listing the CIDR) triggers occupy again; two operator replicas running simultaneously; restoring state from an old snapshot.

Related errors


AI-assisted analysis of cilium/cilium@ac7b90affa (2026-08-31). Data as JSON: /api/errors/74481a208584daf8. Report an issue: GitHub.