MHSanaei/3x-ui · warning

too many outbounds in one request

Error message

too many outbounds in one request (max %d)

What it means

TestOutbounds caps one request at maxBatchItems (50) outbounds to bound the probe batch: each item gets its own loopback port and inbounds entry in a shared xray test process. Exceeding the cap fails the whole request before any probing starts.

Solutions

  1. Split the request into chunks of at most 50 outbounds and aggregate the results client-side
  2. Filter to only the outbounds you actually need tested before batching

Example fix

// before
results, err := svc.TestOutbounds(allOutboundsJSON, url, ctx, mode) // 120 items

// after
const chunk = 50
for i := 0; i < len(items); i += chunk {
    end := min(i+chunk, len(items))
    part, _ := svc.TestOutbounds(marshal(items[i:end]), url, ctx, mode)
    results = append(results, part...)
}
Defensive patterns

Strategy: validation

Validate before calling

const maxBatch = 50
var items []json.RawMessage
_ = json.Unmarshal([]byte(outboundsJSON), &items)
if len(items) > maxBatch {
    // split client-side before calling
    outboundsJSON = marshalN(items, maxBatch)
}
results, err := svc.TestOutbounds(outboundsJSON, ...)

Try / catch

results, err := svc.TestOutbounds(payload, ...)
if err != nil && strings.Contains(err.Error(), "too many outbounds") {
    // chunk and retry; aggregate results
}

Prevention

When it happens

Trigger: Sending an array of 51+ outbounds to the batch test endpoint/API in one call.

Common situations: Selecting 'test all' on a large outbound list where the client batches everything into one request; migrating a big config and probing it wholesale.

Related errors


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

Appendix: source

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

	var ob map[string]any
	if err := json.Unmarshal([]byte(outboundJSON), &ob); err != nil {
		return &TestOutboundResult{Mode: probeModeLabel(mode), Success: false, Error: fmt.Sprintf("Invalid outbound JSON: %v", err)}, nil
	}
	results := s.testOutboundsParsed([]map[string]any{ob}, testURL, allOutboundsJSON, mode)
	return results[0], nil
}

// TestOutbounds probes a JSON array of outbounds and returns one result per
// input, in input order, each carrying the outbound's tag. allOutboundsJSON
// supplies the config context (sockopt.dialerProxy chains); testURL falls
// back to the default probe URL when empty.
func (s *OutboundService) TestOutbounds(outboundsJSON string, testURL string, allOutboundsJSON string, mode string) ([]*TestOutboundResult, error) {
	var raw []json.RawMessage
	if err := json.Unmarshal([]byte(outboundsJSON), &raw); err != nil {
		return nil, fmt.Errorf("invalid outbounds JSON: %w", err)
	}
	if len(raw) > maxBatchItems {
		return nil, fmt.Errorf("too many outbounds in one request (max %d)", maxBatchItems)
	}
	items := make([]map[string]any, len(raw))
	for i, r := range raw {
		var ob map[string]any
		if err := json.Unmarshal(r, &ob); err == nil {
			items[i] = ob
		}
	}
	return s.testOutboundsParsed(items, testURL, allOutboundsJSON, mode), nil
}

// testOutboundsParsed splits items into the TCP lane (direct dials, bounded
// worker pool) and the HTTP lane (one shared temp xray instance), runs both,
// and returns results aligned with items. A nil item marks unparseable input.
func (s *OutboundService) testOutboundsParsed(items []map[string]any, testURL string, allOutboundsJSON string, mode string) []*TestOutboundResult {
	results := make([]*TestOutboundResult, len(items))

	modeLabel := probeModeLabel(mode)

View on GitHub (pinned to ad32144c42)