{"record":{"id":"823e025402ea4a18","repo":"caddyserver/caddy","slug":"deleted-more-than-stored-v-usage-d","errorCode":null,"errorMessage":"deleted more than stored: %#v (usage: %d)","messagePattern":"deleted more than stored: %#v \\(usage: (.+?)\\)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"usagepool.go","lineNumber":192,"sourceCode":"\tif !ok {\n\t\tup.Unlock()\n\t\treturn false, nil\n\t}\n\trefs := upv.refs.Add(-1)\n\tif refs == 0 {\n\t\tdelete(up.pool, key)\n\t\tup.Unlock()\n\t\tupv.RLock()\n\t\tval := upv.value\n\t\tupv.RUnlock()\n\t\tif destructor, ok := val.(Destructor); ok {\n\t\t\terr = destructor.Destruct()\n\t\t}\n\t\tdeleted = true\n\t} else {\n\t\tup.Unlock()\n\t\tif refs < 0 {\n\t\t\tpanic(fmt.Sprintf(\"deleted more than stored: %#v (usage: %d)\",\n\t\t\t\tupv.value, upv.refs.Load()))\n\t\t}\n\t}\n\treturn deleted, err\n}\n\n// References returns the number of references (count of usages) to a\n// key in the pool, and true if the key exists, or false otherwise.\nfunc (up *UsagePool) References(key any) (int, bool) {\n\tup.RLock()\n\tupv, loaded := up.pool[key]\n\tup.RUnlock()\n\tif loaded {\n\t\t// I wonder if it'd be safer to read this value during\n\t\t// our lock on the UsagePool... guess we'll see...\n\t\trefs := upv.refs.Load()\n\t\treturn int(refs), true\n\t}","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/usagepool.go#L174-L210","documentation":"UsagePool is a reference-counted pool (used e.g. for shared listeners/certs). Delete() decrements the counter and deletes at zero; the documented contract is: call Delete exactly as many times as LoadOrStore succeeded. If the count goes negative — more Deletes than Loads — the invariant is broken and UsagePool.Delete panics.","triggerScenarios":"Calling up.Delete(key) more times than up.LoadOrStore(key, ...) returned loaded=true for that key, e.g. deleting in Cleanup() for a config that never loaded, or double-cleanup after a failed provision.","commonSituations":"Module Cleanup() unconditionally deleting a pooled resource it did not store; error paths that clean up after a partial provision; concurrency changes making cleanup run twice.","solutions":["Only call Delete when your LoadOrStore actually loaded/stored the value (check the loaded return, or track a stored flag on your module)","Guard cleanup: defer deleting only if provisioning fully succeeded","Use References(key) to inspect the count when debugging unbalanced usage"],"exampleFix":"// before\nfunc (h *Handler) Cleanup() error {\n    usagePool.Delete(h.key) // runs even if never stored\n    return nil\n}\n// after\nfunc (h *Handler) Cleanup() error {\n    if h.stored {\n        usagePool.Delete(h.key)\n    }\n    return nil\n}","handlingStrategy":"validation","validationCode":"func (up *UsagePool) Refs(key any) int { n, _ := up.References(key); return n }\n\n// call before Delete to avoid underflow\nif refs := pool.Refs(key); refs > 0 {\n    pool.Delete(key)\n}","typeGuard":null,"tryCatchPattern":"defer func() {\n    if r := recover(); r != nil {\n        err = fmt.Errorf(\"UsagePool.Delete underflow: %v\", r)\n    }\n}()\ndeleted, err = up.Delete(key)","preventionTips":["Record whether LoadOrStore stored for you; delete only then","Cleanup() must be symmetric with successful provisions","Use References(key) when auditing ref-count balance"],"tags":["caddy","go","usagepool","reference-counting","lifecycle","panic"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}