XTLS/Xray-core · error

Counter %s already registered.

Error message

Counter %s already registered.

What it means

Returned by stats.Manager.RegisterCounter when a counter with the same name already exists in the manager's registry. Registration is exclusive by design so that metric identity (e.g. 'user>>>email>>>traffic>>>uplink') is unambiguous; the manager offers GetOrRegisterCounter for the idempotent path. The name is interpolated into the message by xray's variadic errors.New.

Source

Thrown at app/stats/stats.go:43

		onlineMaps: make(map[string]*OnlineMap),
		channels:   make(map[string]*Channel),
	}

	return m, nil
}

// Type implements common.HasType.
func (*Manager) Type() interface{} {
	return stats.ManagerType()
}

// RegisterCounter implements stats.Manager.
func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
	m.access.Lock()
	defer m.access.Unlock()

	if _, found := m.counters[name]; found {
		return nil, errors.New("Counter ", name, " already registered.")
	}
	errors.LogDebug(context.Background(), "create new counter ", name)
	c := new(Counter)
	m.counters[name] = c
	return c, nil
}

// GetOrRegisterCounter implements stats.Manager.
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
	m.access.Lock()
	defer m.access.Unlock()

	if c, found := m.counters[name]; found {
		return c, nil
	}
	errors.LogDebug(context.Background(), "create new counter ", name)
	c := new(Counter)
	m.counters[name] = c

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Switch to GetOrRegisterCounter(name), which returns the existing counter instead of erroring.
  2. If the duplicate is unexpected, log the existing registration (VisitCounters) to find which component registered the name first and remove the duplicate call.
  3. When removing a user/handler, call RemoveCounter so a later re-add can register cleanly.

Example fix

// before
c, err := statsManager.RegisterCounter("user>>>a@b.com>>>traffic>>>uplink")

// after
c, err := statsManager.GetOrRegisterCounter("user>>>a@b.com>>>traffic>>>uplink")
Defensive patterns

Strategy: fallback

Validate before calling

if c, err := m.GetOrRegisterCounter(name); err == nil { use(c) } // GetOrRegister never hits this error

Try / catch

c, err := m.RegisterCounter(name)
if err != nil {
    if c2, ok := m.GetCounter(name); ok { c = c2 } else { return err } // fall back to existing
}

Prevention

When it happens

Trigger: Calling RegisterCounter(name) twice with the same name on one Manager instance — typically when a handler is added/removed and re-added (dynamic inbound/outbound users) and startup code re-registers the same counter name.

Common situations: Panels or automation that add users twice; reloading handlers without tearing down previous stats; racing initialization code that registers 'inbound>>>tag>>>traffic>>>downlink' both at server start and when the API adds a user.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/7ee30e3d87fa2765. Report an issue: GitHub.