[BUG] ECONNRESET on streaming connections behind multi-layer NAT (macOS 26.6.1, v2.1.231)

Status Closed — not planned
Reported on v2.1.231
Maintainer reply None cached
Activity 2 comments · opened Aug 13, 2026 · closed Aug 13, 2026

Reopening this as a new issue per the stale-bot instruction on #67091. The same failure is described in #67091 (closed NOT_PLANNED, titled for Windows), #48008 (closed as duplicate, macOS), and #51164 (closed NOT_PLANNED). It still reproduces on current versions, and I have a controlled set of scenarios that isolates it to NAT depth in the path.

Environment

  • Claude Code 2.1.231
  • Node v20.17.0
  • macOS 26.6.1, arm64, MacBook Air M4 (Mac16,12)

Three scenarios

  1. Without the UniFi Travel Router - network setup works, but traffic goes over Tailscale.
  2. With the UniFi Travel Router over Wi-Fi.
  3. With the UniFi Travel Router, where we connect an Ethernet cable and change routing priorities.

Setup

Working from a hotel network. Tailscale is running in all three scenarios with accept-routes on and no exit node selected, so it carries tailnet traffic only and Anthropic traffic never rides it. The variable is a UniFi Travel Router, which when present full-tunnels everything back to a UCG-Fiber at my house over Teleport (WireGuard).

Scenario 1 - no travel router

The laptop associates directly with the hotel SSID. One NAT layer. Tailscale gives me my home resources and everything else egresses the hotel directly. This is stable. I get normal sessions and don't see the reset.

Scenario 2 - travel router over Wi-Fi

The laptop is on the travel router's SSID with Teleport up. Three NAT/conntrack tables now sit in the path of a single streaming socket: hotel gateway, travel router, home gateway. I confirmed the tunnel is actually carrying the traffic, since my egress IP resolves to my home ISP rather than the hotel's.

This is where I get "Connection dropped (ECONNRESET) · Retrying in 23s · attempt 7/10" repeatedly, and it lands during pauses while the model is thinking or writing a long file.

I ruled out MTU and general path health first. DF-set pings pass at a full 1500 bytes to api.anthropic.com, four consecutive 10 MB TLS downloads over the same path completed clean at 2.6-3.3 MB/s, and eight consecutive TLS handshakes to api.anthropic.com all completed in roughly 100 ms. The path is healthy. It is specifically long-lived idle sockets that die.

Scenario 3 - travel router with Ethernet and changed routing priority

Same travel router, but with an Ethernet cable to the laptop and Ethernet moved above Wi-Fi in the macOS network service order. This is not intermittent. Claude Code stops working outright.

The recovery is specific and worth recording. Unplugging the cable and associating directly with the hotel SSID, which is scenario 1, restores connectivity. Falling back to the travel router's Wi-Fi, which is scenario 2, does not. So recovering means leaving the travel router path entirely, not merely changing which interface ranks first. Whatever state this puts the connection into is not cleared by demoting the wired interface on its own.

On my machine Wi-Fi sits at position 14 of 16 in the default service order, below a long list of dock and bridge services, so it is easy to end up with a wired interface ranked first without realizing the ranking changed.

macOS keepalive defaults

The macOS defaults make this worse than the ~346 s figure in #67091 suggests. macOS ships net.inet.tcp.always_keepalive=0 with net.inet.tcp.keepidle=7200000 and net.inet.tcp.keepintvl=75000. A socket that doesn't explicitly request SO_KEEPALIVE gets nothing, and even one that does would wait two hours before its first probe. There is no OS-level safety net, so whichever NAT in the chain has the shortest idle timeout wins by default.

Expected

The streaming socket sets SO_KEEPALIVE with an interval comfortably below the shortest common NAT idle timeout, so idle periods during model inference don't get the flow evicted from an intermediate conntrack table.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗