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

  1. Only call Delete when your LoadOrStore actually loaded/stored the value (check the loaded return, or track a stored flag on your module)
  2. Guard cleanup: defer deleting only if provisioning fully succeeded
  3. 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

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


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/823e025402ea4a18. Report an issue: GitHub.