MHSanaei/3x-ui · warning

xray is already running

Error message

xray is already running

What it means

process.Start() refuses to launch a second xray-core child from the same process object when IsRunning() is already true. The check reads the PID/exit state under the hood, so this fires on double-start races or reusing a process instance that is still alive. It protects against two xray binaries fighting over the same config/API port.

Source

Thrown at internal/xray/process.go:518

func (p *process) refreshVersion() {
	version := "Unknown"
	ctx, cancel := context.WithTimeout(context.Background(), xrayVersionTimeout)
	defer cancel()
	cmd := exec.CommandContext(ctx, GetBinaryPath(), "-version")
	if data, err := cmd.Output(); err == nil {
		if datas := bytes.Split(data, []byte(" ")); len(datas) > 1 {
			version = string(datas[1])
		}
	}
	p.mu.Lock()
	p.version = version
	p.mu.Unlock()
}

// Start launches the Xray process with the current configuration.
func (p *process) Start() (err error) {
	if p.IsRunning() {
		return errors.New("xray is already running")
	}

	defer func() {
		if err != nil {
			logger.Error("Failure in running xray-core process: ", err)
			p.setExitErr(err)
		}
	}()

	data, err := json.MarshalIndent(p.config, "", "  ")
	if err != nil {
		return common.NewErrorf("Failed to generate XRAY configuration files: %v", err)
	}

	err = os.MkdirAll(config.GetLogFolder(), 0o770)
	if err != nil {
		logger.Warningf("Failed to create log folder: %s", err)
	}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Check IsRunning() before calling Start, or serialize starts behind the existing service-level lock (RestartXray already holds it).
  2. If you truly want a fresh core, call Stop (and wait) before Start — i.e. use RestartXray instead of Start.
  3. Audit for duplicate start paths (startup task + manual API) firing at the same time.

Example fix

// before
err := p.Start() // "xray is already running"

// after
if p.IsRunning() {
    err = p.Stop()
    if err != nil { return err }
}
err = p.Start()
Defensive patterns

Strategy: validation

Validate before calling

if p.IsRunning() {
    // already started; nothing to do or use Restart instead
    return nil
}
return p.Start()

Try / catch

if err := p.Start(); err != nil && strings.Contains(err.Error(), "already running") {
    return nil // idempotent start
} else if err != nil {
    return err
}

Prevention

When it happens

Trigger: Two concurrent RestartXray calls, or Start called from both the startup task and an API handler; re-Start after a hot-reload attempt that did not go through Stop; racing the crash-restart handler.

Common situations: Concurrent admin operations triggering restarts; custom scripts invoking the start path while the panel already started xray; test code that starts the same process object twice.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/1997cf3b4d0f1d63. Report an issue: GitHub.