{"record":{"id":"a0cb1948a5ddcaf0","repo":"tailscale/tailscale","slug":"updates-no-longer-apply-to-head-based-on-x-but-h","errorCode":null,"errorMessage":"updates no longer apply to head: based on %x but head is %x","messagePattern":"updates no longer apply to head: based on %x but head is %x","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tka/builder.go","lineNumber":165,"sourceCode":"\t\tparent, hasParent := aum.Parent()\n\t\tif !hasParent {\n\t\t\t// We've hit the genesis update, so the chain is shorter than the interval to checkpoint at.\n\t\t\tneedCheckpoint = false\n\t\t\tbreak\n\t\t}\n\t\tcursor = parent\n\t}\n\n\tif needCheckpoint {\n\t\tif err := b.generateCheckpoint(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"generating checkpoint: %v\", err)\n\t\t}\n\t}\n\n\t// Check no AUMs were applied in the meantime\n\tif len(b.out) > 0 {\n\t\tif parent, _ := b.out[0].Parent(); parent != b.a.Head() {\n\t\t\treturn nil, fmt.Errorf(\"updates no longer apply to head: based on %x but head is %x\", parent, b.a.Head())\n\t\t}\n\t}\n\treturn b.out, nil\n}\n\n// NewUpdater returns a builder you can use to make changes to\n// the tailnet key authority.\n//\n// The provided signer function, if non-nil, is called with each update\n// to compute and apply signatures.\n//\n// Updates are specified by calling methods on the returned UpdatedBuilder.\n// Call Finalize() when you are done to obtain the specific update messages\n// which actuate the changes.\nfunc (a *Authority) NewUpdater(signer Signer) *UpdateBuilder {\n\treturn &UpdateBuilder{\n\t\ta:      a,\n\t\tsigner: signer,","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/tka/builder.go#L147-L183","documentation":"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.","triggerScenarios":"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.","commonSituations":"Parallel admin operations against one tailnet lock; multi-process access to the same TKA state without coordination.","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"],"exampleFix":"// before: one-shot update\nb := tka.NewUpdater(a, signer)\nb.SetKey(newKey)\naums, err := b.Finalize() // fails under concurrency\n\n// after: rebuild on conflict\nfor attempt := 0; attempt < 3; attempt++ {\n    b := tka.NewUpdater(a, signer) // fresh snapshot of head\n    b.SetKey(newKey)               // re-apply mutation\n    aums, err = b.Finalize()\n    if err == nil {\n        break\n    }\n    if !strings.Contains(err.Error(), \"no longer apply to head\") {\n        return err\n    }\n    // state advanced elsewhere; loop picks up the new head\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"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.","preventionTips":["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"],"tags":["tka","tailnet-lock","concurrency","consistency"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}