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
- Retry: rebuild the updater against the now-current authority state and reapply the mutation
- Serialize writers (single admin path or an external lock) so only one builder is in flight per state
- 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
- Serialize TKA mutations behind a single writer or lock so only one builder is in flight
- Keep builder lifetimes short: create, mutate, Finalize immediately
- Never persist a chain produced against a stale head
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
- error: %w
- error %w: %s
- sending verify-deeplink: %w
- sending generate-recovery-aum: %w
- sending cosign-recovery-aum: %w
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/a0cb1948a5ddcaf0.
Report an issue: GitHub.