Skip to content

Instantly share code, notes, and snippets.

@arutkayb
Created June 23, 2026 06:41
Show Gist options
  • Select an option

  • Save arutkayb/f56e7a4e1d8b50a6064645c327749fbc to your computer and use it in GitHub Desktop.

Select an option

Save arutkayb/f56e7a4e1d8b50a6064645c327749fbc to your computer and use it in GitHub Desktop.
Workaround for Claude Code API Error: Unable to connect to API (ECONNRESET)

claude-upload-limit-workaround

Fix for Claude Code (and any large HTTPS upload) failing with API Error: Unable to connect to API (ECONNRESET) on a specific home network.

Symptom

  • Short Claude requests always work; long sessions reset after ~5–13 min.
  • Works fine on a phone hotspot, fails on home Wi-Fi.
  • Debug log shows the same request retried 11 times, each dying at a consistent ~15 s, then code=ECONNRESET.

Root cause

It is not MTU, IPv6, a proxy, the client, or idle-connection reaping — all ruled out by testing. It is the upload direction.

Every Claude request uploads the full conversation context as the POST body.

  1. macOS auto-tunes the TCP send buffer up to net.inet.tcp.autosndbufmax (stock default 4 MB).
  2. A large body lets a single connection burst multiple MB into the DSL modem's upstream buffer all at once.
  3. That buffer tail-drops at ~2.29 MB on this line. The flow stalls and the server resets it ~15 s later → Claude retries 11× → ECONNRESET.

Small requests never burst that far, so they succeed. The hotspot's path absorbs the burst differently, so it succeeds too. The uplink itself is fast (~24 Mbit measured) — the only problem is the unpaced burst.

Why this fix works

Capping the send buffer to 256 KiB bounds in-flight data far below the 2.29 MB overflow point, so a single connection can never overflow the modem buffer. The link's bandwidth-delay product is only ~22 KB, so the cap costs zero throughput.

How it was confirmed

A 100 MB upload to https://speed.cloudflare.com/__up reproduced it exactly: reset at 2.29 MB / 15 s with the default 4 MB buffer; full 100 MB at 3 MB/s with the 256 KiB cap. Throttled curl --limit-rate uploads also always succeeded, proving pacing — not size or time — was the fix.

Usage

upload-fix on        # apply cap + install LaunchDaemon (persists across reboot)
upload-fix off       # remove LaunchDaemon + restore macOS defaults
upload-fix status    # show current state (no sudo)
upload-fix on 131072 # tighter 128 KiB cap (if a heavily-parallel session resets)

on / off self-elevate (one sudo prompt). status is read-only.

Values

setting stock macOS this fix
net.inet.tcp.autosndbufmax 4194304 262144
net.inet.tcp.sendspace 131072 262144

Proper long-term fix

This is a CPE/line trait — a modern line shouldn't tail-drop a burst like this. The device-independent fix is a router doing SQM / fq_codel (e.g. put the Speedport in bridge mode behind an OpenWrt router, or a Fritz!Box with shaping). Until then, this per-host cap keeps things working.

#!/bin/sh
# upload-fix — cap the TCP send buffer so an unpaced upload burst can't
# overflow the DSL modem's upstream buffer. Without this, large request
# bodies (e.g. long Claude Code sessions) reset mid-upload with ECONNRESET
# on the home line; small ones work. Cap bounds in-flight data well under
# the modem buffer (~2.29 MB here) at zero throughput cost.
#
# Usage:
# upload-fix on [capbytes] apply cap + install LaunchDaemon (persists reboot)
# upload-fix off remove LaunchDaemon + restore macOS defaults
# upload-fix status show current state
#
# Default cap 262144 (256 KiB). If a heavily-parallel session ever resets
# again, re-run: upload-fix on 131072
set -eu
PLIST=/Library/LaunchDaemons/com.local.tcpsndbuf.plist
LABEL=com.local.tcpsndbuf
CAP="${2:-262144}" # 256 KiB
DEFAULT_MAX=4194304 # macOS stock autosndbufmax
DEFAULT_SND=131072 # macOS stock sendspace
SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
need_root() { [ "$(id -u)" -eq 0 ] || exec sudo "$SELF" "$@"; }
case "${1:-}" in
on)
need_root "$@"
sysctl -w net.inet.tcp.autosndbufmax="$CAP" net.inet.tcp.sendspace="$CAP" >/dev/null
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>$LABEL</string>
<key>ProgramArguments</key>
<array><string>/usr/sbin/sysctl</string><string>-w</string>
<string>net.inet.tcp.autosndbufmax=$CAP</string>
<string>net.inet.tcp.sendspace=$CAP</string></array>
<key>RunAtLoad</key><true/>
</dict></plist>
EOF
chown root:wheel "$PLIST"; chmod 644 "$PLIST"
launchctl bootstrap system "$PLIST" 2>/dev/null \
|| launchctl load -w "$PLIST" 2>/dev/null || true
echo "upload-fix ON (cap=$CAP bytes, persists across reboot)"
sysctl net.inet.tcp.autosndbufmax net.inet.tcp.sendspace
;;
off)
need_root "$@"
launchctl bootout system "$PLIST" 2>/dev/null \
|| launchctl unload -w "$PLIST" 2>/dev/null || true
rm -f "$PLIST"
sysctl -w net.inet.tcp.autosndbufmax="$DEFAULT_MAX" net.inet.tcp.sendspace="$DEFAULT_SND" >/dev/null
echo "upload-fix OFF (restored macOS defaults)"
sysctl net.inet.tcp.autosndbufmax net.inet.tcp.sendspace
;;
status)
printf 'launchdaemon: '; [ -f "$PLIST" ] && echo "installed ($PLIST)" || echo "not installed"
sysctl net.inet.tcp.autosndbufmax net.inet.tcp.sendspace
;;
*)
echo "usage: $(basename "$0") on|off|status [capbytes]" >&2
exit 2
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment