XTLS/Xray-core · error

MaxSendingWindow must be >= Mtu

Error message

MaxSendingWindow must be >= Mtu

What it means

Thrown by KCPConfig.Build() when GetSendingBufferSize() returns 0. That helper derives the send buffer size from MaxSendingWindow (in MB) combined with Mtu; if the window is too small to hold even one MTU-sized packet, the buffer size computes to zero and the transport could never queue a packet. Hence the message 'MaxSendingWindow must be >= Mtu'.

Source

Thrown at infra/conf/transport_method.go:572

	}
	if c.CwndMultiplier != nil {
		config.CwndMultiplier = *c.CwndMultiplier
	}
	if c.MaxSendingWindow != nil {
		config.MaxSendingWindow = *c.MaxSendingWindow
	}

	if config.Mtu < 21 {
		return nil, errors.New("Mtu must be at least 21").AtError()
	}
	if config.Tti < 10 || config.Tti > 1000 {
		return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
	}
	if config.CwndMultiplier < 1 {
		return nil, errors.New("CwndMultiplier must be at least 1").AtError()
	}
	if config.GetSendingBufferSize() == 0 {
		return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
	}

	return config, nil
}

type GRPCConfig struct {
	Authority           string `json:"authority"`
	ServiceName         string `json:"serviceName"`
	MultiMode           bool   `json:"multiMode"`
	IdleTimeout         int32  `json:"idle_timeout"`
	HealthCheckTimeout  int32  `json:"health_check_timeout"`
	PermitWithoutStream bool   `json:"permit_without_stream"`
	InitialWindowsSize  int32  `json:"initial_windows_size"`
	UserAgent           string `json:"user_agent"`
}

func (g *GRPCConfig) Build() (proto.Message, error) {
	if g.IdleTimeout <= 0 {

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Set maxSendingWindow large enough that (value_in_MB * 1e6) / mtu >= 1, in practice use 10-1024.
  2. Verify mtu is a sane value (1200-1500), not something enormous.
  3. Remove maxSendingWindow to let the default apply.

Example fix

// before
"kcpSettings": { "mtu": 1350, "maxSendingWindow": 0 }
// after
"kcpSettings": { "mtu": 1350, "maxSendingWindow": 50 }
Defensive patterns

Strategy: validation

Validate before calling

mtu := 1350
if kcp.Mtu != nil { mtu = int(*kcp.Mtu) }
windowBytes := uint64(0)
if kcp.MaxSendingWindow != nil { windowBytes = uint64(*kcp.MaxSendingWindow) * 1024 * 1024 }
if windowBytes != 0 && windowBytes/uint64(mtu) == 0 {
    return errors.New("maxSendingWindow too small for mtu")
}

Try / catch

if _, err := kcpCfg.Build(); err != nil && strings.Contains(err.Error(), "MaxSendingWindow") {
    kcp.MaxSendingWindow = nil // use default and rebuild
    _, err = kcpCfg.Build()
    if err != nil { return err }
}

Prevention

When it happens

Trigger: Setting maxSendingWindow (MB) so small that windowMB*1e6/mtu rounds to 0 — e.g. mtu 1350 with maxSendingWindow 0 or a fraction too small — or leaving maxSendingWindow unset while a huge custom mtu makes the division floor to zero.

Common situations: Users setting maxSendingWindow: 0 thinking it means 'unlimited', or pairing a very large mtu with the minimum window size.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/5c6034ea744e42bd. Report an issue: GitHub.