MHSanaei/3x-ui · error

Failed to reserve test ports: %w

Error message

Failed to reserve test ports: %w

What it means

Before spawning the shared xray test process, runHTTPProbeBatch reserves one loopback listener per item via reserveLoopbackPorts (net.Listen on 127.0.0.1:0). Failure means the OS refused new loopback listeners: ephemeral port exhaustion, file-descriptor limit (ulimit -n), or a broken loopback interface. It is classified environmental (retryPerItem=false) — per-item retries would fail identically.

Source

Thrown at internal/web/service/outbound/probe_http.go:280

	for _, it := range httpItems {
		if _, ferr := runHTTPProbeBatch([]*httpBatchItem{it}, allOutbounds, testURL, realDelay); ferr != nil {
			it.result.Success = false
			it.result.Error = ferr.Error()
		}
	}
	return results
}

// runHTTPProbeBatch makes one shared-process attempt for the given items,
// writing per-request outcomes into the items' results. It returns a non-nil
// error only when the process never became usable; retryPerItem reports
// whether splitting the batch into per-item instances could help (true for
// start failures / early exits that a poisoned config would explain, false
// for environmental failures like a missing binary or no free ports).
func runHTTPProbeBatch(items []*httpBatchItem, allOutbounds []any, testURL string, realDelay bool) (retryPerItem bool, err error) {
	ports, release, err := reserveLoopbackPorts(len(items))
	if err != nil {
		return false, fmt.Errorf("Failed to reserve test ports: %w", err)
	}
	defer release()

	cfg := buildBatchTestConfig(items, allOutbounds, ports)

	configPath, err := createTestConfigPath()
	if err != nil {
		return false, fmt.Errorf("Failed to create test config path: %w", err)
	}
	defer os.Remove(configPath)

	proc := newBatchProcess(cfg, configPath)
	defer func() {
		if proc.IsRunning() {
			_ = proc.Stop()
		}
	}()

View on GitHub (pinned to ad32144c42)

Solutions

  1. Raise the file-descriptor limit (ulimit -n / LimitNOFILE) for the panel process
  2. Reduce the batch size so fewer ports are reserved at once
  3. Check for socket/listener leaks: ss -s, lsof -p <pid> | wc -l
  4. If ephemeral ports are exhausted, widen net.ipv4.ip_local_port_range or wait for TIME_WAIT to drain

Example fix

# before (container default)
docker run ... 3x-ui   # ulimit 1024, batch probe fails

# after
docker run --ulimit nofile=65536:65536 ... 3x-ui
# or in systemd unit:
[Service]
LimitNOFILE=65536
Defensive patterns

Strategy: fallback

Validate before calling

// rough precheck before probing
if ulimit := runtimeNumFDs(); ulimit > 0 && ulimit < 1024 {
    // warn: port/FD reservation may fail; raise limit or reduce batch
}

Try / catch

results, err := runHTTPProbeBatch(items, all, url, true)
if err != nil && strings.Contains(err.Error(), "reserve test ports") {
    // environmental: do NOT retry per-item; surface 'raise ulimit / free ports' to user
}

Prevention

When it happens

Trigger: Probing a batch when the process is out of FDs or the ephemeral port range is exhausted (e.g. after heavy churn of xray test processes or other listeners); containers with very low ulimit.

Common situations: Docker container with default 1024 FD soft limit while running frequent outbound tests; a leak of sockets/listeners elsewhere; heavy cron probing on a small VPS.

Related errors


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