MHSanaei/3x-ui · warning

too many outbounds in one request (max %d)

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.

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)

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.