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
- Raise the file-descriptor limit (ulimit -n / LimitNOFILE) for the panel process
- Reduce the batch size so fewer ports are reserved at once
- Check for socket/listener leaks: ss -s, lsof -p <pid> | wc -l
- 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
- Run the panel with LimitNOFILE >= 65536
- Keep probe batches small in cron jobs to bound concurrent loopback listeners
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
- Failed to create test config path: %w
- Failed to start test xray instance: %w
- outbound subscription response body exceeds size limit (limi
- node port must be 1-65535
- HTTP %d from remote panel
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/eb2e559b4e39d2c5.
Report an issue: GitHub.