XTLS/Xray-core · warning · errors.Error

outbound connection closed

Error message

outbound connection closed

What it means

outboundConn is a lazily-dialed, mutex-guarded connection wrapper inside the DNS proxy. Write() checks the closed flag first; once Close() has run, any subsequent Write returns this error. It is an internal lifecycle error indicating use-after-close on the pooled DNS outbound connection, not a network failure.

Source

Thrown at proxy/dns/dns.go:459

	connReady chan struct{}
	closed    bool
}

func (c *outboundConn) dial() error {
	conn, err := c.dialer()
	if err != nil {
		return err
	}
	c.conn = conn
	c.connReady <- struct{}{}
	return nil
}

func (c *outboundConn) Write(b []byte) (int, error) {
	c.access.Lock()
	if c.closed {
		c.access.Unlock()
		return 0, errors.New("outbound connection closed")
	}

	if c.conn == nil {
		if err := c.dial(); err != nil {
			c.access.Unlock()
			errors.LogWarningInner(context.Background(), err, "failed to dial outbound connection")
			return 0, err
		}
	}

	c.access.Unlock()

	return c.conn.Write(b)
}

func (c *outboundConn) Read(b []byte) (int, error) {
	c.access.Lock()
	if c.closed {

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Treat as a symptom of a write-after-close race during teardown; check whether it correlates with shutdown/reload timing
  2. Upgrade to the latest Xray — connection lifecycle races in the DNS handler get fixed over time; report a reproducible pattern to the issue tracker
  3. If embedding Xray, drain in-flight handlers (cancel context, wait for goroutines) before closing outbound connections
  4. No end-user config triggers it directly; ensure DNS upstreams are healthy so the pool is not being torn down under errors

Example fix

// before (embedding, racy)
conn.Close()
wg.Wait() // writers may still Write -> outbound connection closed

// after
cancel()
wg.Wait()
conn.Close()
Defensive patterns

Strategy: try-catch

Validate before calling

c.access.Lock(); closed := c.closed; c.access.Unlock()
if closed { return io.ErrClosedPipe /* skip write */ }

Type guard

func outboundConnUsable(c *outboundConn) bool { c.access.Lock(); defer c.access.Unlock(); return !c.closed }

Try / catch

if _, err := c.Write(b); err != nil {
    if err.Error() == "outbound connection closed" { reinitializeConn(); return retry() }
    return err
}

Prevention

When it happens

Trigger: A goroutine still holding the outboundConn calls Write after another goroutine completed Close() — e.g. an in-flight DNS response write racing connection teardown at shutdown or after a fatal upstream error.

Common situations: Query goroutines not fully drained before closing the outboundConn during handler shutdown; races surfaced under load tests or instance reload; generally only observable in logs as a symptom, not a crash cause.

Understand the failure class

Related errors


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