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
- Treat as a symptom of a write-after-close race during teardown; check whether it correlates with shutdown/reload timing
- Upgrade to the latest Xray — connection lifecycle races in the DNS handler get fixed over time; report a reproducible pattern to the issue tracker
- If embedding Xray, drain in-flight handlers (cancel context, wait for goroutines) before closing outbound connections
- 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
- Cancel contexts and wait for handler goroutines before closing shared conns
- Use -race in tests for embedding code that touches the DNS handler
- Upgrade Xray regularly; lifecycle races here get patched upstream
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
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- failed to resolve ip for target ${domain}
- unknown action: {}
- rCode out of range: {}
- legacy nonIPQuery and blockTypes cannot be mixed with rules
- unknown nonIPQuery: {}
AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15).
Data as JSON: /api/errors/438104863fc950f9.
Report an issue: GitHub.