caddyserver/caddy · critical
deleted more than stored: %#v (usage: %d)
Error message
deleted more than stored: %#v (usage: %d)
What it means
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.
Source
Thrown at usagepool.go:192
if !ok {
up.Unlock()
return false, nil
}
refs := upv.refs.Add(-1)
if refs == 0 {
delete(up.pool, key)
up.Unlock()
upv.RLock()
val := upv.value
upv.RUnlock()
if destructor, ok := val.(Destructor); ok {
err = destructor.Destruct()
}
deleted = true
} else {
up.Unlock()
if refs < 0 {
panic(fmt.Sprintf("deleted more than stored: %#v (usage: %d)",
upv.value, upv.refs.Load()))
}
}
return deleted, err
}
// References returns the number of references (count of usages) to a
// key in the pool, and true if the key exists, or false otherwise.
func (up *UsagePool) References(key any) (int, bool) {
up.RLock()
upv, loaded := up.pool[key]
up.RUnlock()
if loaded {
// I wonder if it'd be safer to read this value during
// our lock on the UsagePool... guess we'll see...
refs := upv.refs.Load()
return int(refs), true
}View on GitHub (pinned to 50e54ee279)
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
Example fix
// before
func (h *Handler) Cleanup() error {
usagePool.Delete(h.key) // runs even if never stored
return nil
}
// after
func (h *Handler) Cleanup() error {
if h.stored {
usagePool.Delete(h.key)
}
return nil
} Defensive patterns
Strategy: validation
Validate before calling
func (up *UsagePool) Refs(key any) int { n, _ := up.References(key); return n }
// call before Delete to avoid underflow
if refs := pool.Refs(key); refs > 0 {
pool.Delete(key)
} Try / catch
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("UsagePool.Delete underflow: %v", r)
}
}()
deleted, err = up.Delete(key) Prevention
- Record whether LoadOrStore stored for you; delete only then
- Cleanup() must be symmetric with successful provisions
- Use References(key) when auditing ref-count balance
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- malformed tag on field %s: %v
- missing 'namespace' key in struct tag on field %s
- unable to determine module name without inline_key when type
- unable to determine module name without inline_key because t
- expected ModuleMap because inline_key is empty; but we do no
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/823e025402ea4a18.
Report an issue: GitHub.