{"record":{"id":"438104863fc950f9","repo":"XTLS/Xray-core","slug":"outbound-connection-closed","errorCode":null,"errorMessage":"outbound connection closed","messagePattern":"outbound connection closed","errorType":"exception","errorClass":"errors.Error","httpStatus":null,"severity":"warning","filePath":"proxy/dns/dns.go","lineNumber":459,"sourceCode":"\tconnReady chan struct{}\n\tclosed    bool\n}\n\nfunc (c *outboundConn) dial() error {\n\tconn, err := c.dialer()\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.conn = conn\n\tc.connReady <- struct{}{}\n\treturn nil\n}\n\nfunc (c *outboundConn) Write(b []byte) (int, error) {\n\tc.access.Lock()\n\tif c.closed {\n\t\tc.access.Unlock()\n\t\treturn 0, errors.New(\"outbound connection closed\")\n\t}\n\n\tif c.conn == nil {\n\t\tif err := c.dial(); err != nil {\n\t\t\tc.access.Unlock()\n\t\t\terrors.LogWarningInner(context.Background(), err, \"failed to dial outbound connection\")\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\tc.access.Unlock()\n\n\treturn c.conn.Write(b)\n}\n\nfunc (c *outboundConn) Read(b []byte) (int, error) {\n\tc.access.Lock()\n\tif c.closed {","sourceCodeStart":441,"sourceCodeEnd":477,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/proxy/dns/dns.go#L441-L477","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (embedding, racy)\nconn.Close()\nwg.Wait() // writers may still Write -> outbound connection closed\n\n// after\ncancel()\nwg.Wait()\nconn.Close()","handlingStrategy":"try-catch","validationCode":"c.access.Lock(); closed := c.closed; c.access.Unlock()\nif closed { return io.ErrClosedPipe /* skip write */ }","typeGuard":"func outboundConnUsable(c *outboundConn) bool { c.access.Lock(); defer c.access.Unlock(); return !c.closed }","tryCatchPattern":"if _, err := c.Write(b); err != nil {\n    if err.Error() == \"outbound connection closed\" { reinitializeConn(); return retry() }\n    return err\n}","preventionTips":["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"],"tags":["go","xray","dns","concurrency","lifecycle"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}