tailscale/tailscale · error

updates no longer apply to head: based on %x but head is %x

Error message

updates no longer apply to head: based on %x but head is %x

What it means

Builder-based TKA updates are optimistic: the AUM chain is computed against the authority's head when the builder was created, and Finalize re-checks that the first output AUM's parent still equals a.Head(). If another writer committed in the meantime, applying the chain would fork the chain, so Finalize aborts, printing both the builder's base and the current head.

Source

Thrown at tka/builder.go:165

		parent, hasParent := aum.Parent()
		if !hasParent {
			// We've hit the genesis update, so the chain is shorter than the interval to checkpoint at.
			needCheckpoint = false
			break
		}
		cursor = parent
	}

	if needCheckpoint {
		if err := b.generateCheckpoint(); err != nil {
			return nil, fmt.Errorf("generating checkpoint: %v", err)
		}
	}

	// Check no AUMs were applied in the meantime
	if len(b.out) > 0 {
		if parent, _ := b.out[0].Parent(); parent != b.a.Head() {
			return nil, fmt.Errorf("updates no longer apply to head: based on %x but head is %x", parent, b.a.Head())
		}
	}
	return b.out, nil
}

// NewUpdater returns a builder you can use to make changes to
// the tailnet key authority.
//
// The provided signer function, if non-nil, is called with each update
// to compute and apply signatures.
//
// Updates are specified by calling methods on the returned UpdatedBuilder.
// Call Finalize() when you are done to obtain the specific update messages
// which actuate the changes.
func (a *Authority) NewUpdater(signer Signer) *UpdateBuilder {
	return &UpdateBuilder{
		a:      a,
		signer: signer,

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Retry: rebuild the updater against the now-current authority state and reapply the mutation
  2. Serialize writers (single admin path or an external lock) so only one builder is in flight per state
  3. Treat this as a safe abort - nothing was committed; never force-apply the stale chain

Example fix

// before: one-shot update
b := tka.NewUpdater(a, signer)
b.SetKey(newKey)
aums, err := b.Finalize() // fails under concurrency

// after: rebuild on conflict
for attempt := 0; attempt < 3; attempt++ {
    b := tka.NewUpdater(a, signer) // fresh snapshot of head
    b.SetKey(newKey)               // re-apply mutation
    aums, err = b.Finalize()
    if err == nil {
        break
    }
    if !strings.Contains(err.Error(), "no longer apply to head") {
        return err
    }
    // state advanced elsewhere; loop picks up the new head
}
Defensive patterns

Strategy: retry

Try / catch

Wrap Finalize in a bounded rebuild-and-retry loop: on an error containing 'no longer apply to head', discard the chain, reload the authority's current head, rebuild the updater, and reapply the mutation; bail out on any other error or after N attempts so a genuine conflict is surfaced, not spun on.

Prevention

When it happens

Trigger: Two updaters started from the same authority state; one commits first and the other's Finalize now sees out[0].Parent() != head - e.g. concurrent key additions or threshold changes.

Common situations: Parallel admin operations against one tailnet lock; multi-process access to the same TKA state without coordination.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/a0cb1948a5ddcaf0. Report an issue: GitHub.