Python · intermediate
Python ConnectionResetError: distinguish peer reset from local shutdown
Python raises ConnectionResetError when an established socket is closed by the peer with a TCP RST or when the local host tears the connection down before the application expects it. This guide shows engineers how to separate peer-reset from local-shutdown signals using errno, WSAECONNRESET, and peer address evidence, then how to verify each path before changing code.
The symptoms
- •Traceback ends with ConnectionResetError: [Errno 104] Connection reset by peer while a long-lived socket, HTTP keep-alive session, or websocket was idle or mid-transfer.
- •Windows-only traceback reports ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host, often from asyncio, requests, urllib3, or socket.recv calls.
- •The error appears immediately after the server closes a session due to idle timeout, TLS renegotiation, or graceful shutdown, and the client never observes a clean close handshake.
- •Production logs show ConnectionResetError only on the first request after a process restart, on the first request after a long pause, or only when traffic crosses a specific load balancer, NAT, or proxy hop.
- •Retry loops that swallow ConnectionResetError as a generic IOError keep failing at the same rate, indicating the close is not transient on the application side.
Likely causes
- •The remote peer sent a TCP RST segment because its socket had already been closed, its application called abortive close, or its keepalive threshold was exceeded before the client read.
- •A stateful network element (load balancer, NAT gateway, sidecar proxy, cloud egress) silently evicted the flow from its translation table and replied with RST to the next client packet.
- •The local process closed or replaced its socket handle (process restart, fd reuse, server bind to the same port) while the peer still held the old four-tuple open and sent data into it.
- •The local stack tore the connection down during graceful shutdown, interpreter teardown, or signal handling, and the peer saw a RST instead of a FIN because the socket buffer was discarded.
- •TLS layer errors (certificate validation failure, protocol version mismatch, ALPN negotiation abort) caused the underlying TCP socket to be reset before the application saw a clean SSL error.
First ten minutes
- 01Capture errno from the exception (e.g. except ConnectionResetError as e: e.errno, e.strerror) and record whether the value is 104 on POSIX or 10054 on Windows, which is the first discriminator between RST-shaped close and other IO errors.
- 02Record the local and remote socket address associated with the failure (sock.getpeername(), sock.getsockname()) at the moment of the exception so the four-tuple can be matched against server access logs.
- 03Mark whether the failing call was a read (recv, read), a write (send, write), or a higher-level operation, because ECONNRESET surfaces most reliably on the next read after a peer-side abort.
- 04Compare the timestamp of the reset to the server's last successful response on the same connection; a reset that arrives immediately after a 200 response suggests a server-side abort rather than a mid-stream timeout.
- 05Check whether the error correlates with process restart, deployment, configuration reload, or signal receipt on the client side, which indicates local shutdown rather than remote RST.
Evidence to collect
- •The exact exception class and errno value (104 / WSAECONNRESET 10054) plus the OS string returned, which is the minimum signal that a RST was received.
- •The peer address and local address of the failing socket, plus the connection's start time and last successful I/O time, so the flow can be located in server or proxy logs.
- •Server-side records on the same peer IP and port showing whether the close was a clean FIN, an abortive close, or an idle-timeout eviction during the relevant window.
- •Network element logs (load balancer, NAT, proxy) for the same five-tuple showing session age, bytes transferred, and whether the element issued a RST.
- •Application-side signals (SIGTERM, SIGHUP, interpreter shutdown, atexit handlers) active within seconds of the failure, which implicate local shutdown rather than peer reset.
Where to look
- •The Python socket boundary: the syscall that raised the error, and the exception's errno attribute, which is the only authoritative signal of a received RST.
- •The remote socket boundary: the server's access log, application log, and close-path code on the matching peer IP and source port, to see whether the server issued FIN or RST.
- •The middlebox boundary: load balancer connection tables, NAT translation timeouts, and sidecar proxy session logs, since RSTs that originate there will not appear in the server's log.
- •The TLS boundary: TLS alert records and certificate validation errors above the socket, because a TLS-layer abort can surface as ConnectionResetError on the read path.
- •The process boundary: client shutdown sequence, file descriptor reuse, and graceful-stop handlers, where local closes can be misclassified as peer resets.
Diagnostic steps
- 01Branch on the exception's errno: if errno is 104 (POSIX) or WSAECONNRESET (Windows), classify the event as a received TCP RST until evidence contradicts it; if errno is EBADF, ENOTCONN, or EPIPE, the cause is local and a different diagnosis applies.
- 02Correlate the peer address and source port against the remote server's connection log in the failure window to determine whether the server recorded a clean close, an error close, or no close at all.
- 03If the server has no record of the close, inspect the load balancer or proxy in front of it for an evicted session; a middlebox RST will not appear in either endpoint's application log.
- 04If the server records an abortive close or application error at the same timestamp, attribute the RST to the peer application and treat it as a peer-reset, not a network failure.
- 05If the client received the error during shutdown, signal handling, or fd reuse, classify the event as a local shutdown that the peer translated into a RST, and fix the close path on the client side.
- 06If the error only occurs inside a TLS handshake or renegotiation, capture the TLS alert from the layer above the socket and treat the RST as a symptom, not the root cause.
Common mistakes
- •Assuming every ConnectionResetError is a transient network blip and adding a retry loop, which silently multiplies load on a peer that has already decided to drop the session.
- •Catching ConnectionResetError as a bare Exception or OSError without inspecting errno, which conflates peer RST with local EBADF and EPIPE and hides the real boundary.
- •Reading the error as 'connection refused' and looking for an unbound port, when ECONNRESET specifically means a previously established connection was terminated, not that a connect() failed.
- •Ignoring the local shutdown path and assuming the peer is at fault, even when the reset only appears during the client's own restart, reload, or signal handling.
- •Trusting client-side logs alone and skipping server-side correlation, which leaves middlebox resets invisible because the server's connection table will not show them.
Safe fixes
- •If evidence shows a peer RST from a server-side abort, fix the server's close path to send a clean FIN and drain its write buffer, then add a client-side retry that gates on the confirmed errno rather than on any IOError.
- •If evidence shows a middlebox eviction, raise the idle timeout on the load balancer or NAT to exceed the longest expected gap between client requests, or move to a protocol that tolerates eviction (HTTP/2 with GOAWAY handling).
- •If evidence shows a local shutdown leaking RSTs, replace abrupt socket closes with shutdown(SHUT_RDWR) followed by a bounded drain and a context-managed close, so the peer observes FIN instead of RST.
- •If the error originates inside a TLS handshake, surface the TLS alert above the socket so the application can log a specific cause rather than masking it as ConnectionResetError.
- •Add a single, narrow except ConnectionResetError handler that records errno, peer address, and the failing operation, so future occurrences can be classified automatically rather than re-diagnosed by hand.
Prove the fix
- 01Repeat the original connection workflow across the same peer and network path and confirm the reset no longer occurs, or is classified as an expected peer-close event.
- 02Confirm the client releases its socket and retry state after a reset, then completes a later request without a leaked connection or duplicate side effect.
- 03Run the regression test with a controlled peer close and a normal response, verifying that the two cases produce distinct, documented outcomes.
Prevention and next steps
- •Standardize on a single close path in client code: shutdown(SHUT_RDWR) with a bounded drain inside a context manager, so peers always see FIN and never an unannounced RST.
- •Keep idle and read timeouts on the server shorter than any middlebox eviction timeout on the path, so the server closes cleanly before a NAT or load balancer can RST the flow.
- •Tag every ConnectionResetError catch site with the errno and the failing operation, so the next incident can be classified as peer-reset or local-shutdown without re-reading the traceback.
- •Monitor the rate of errno-104 and WSAECONNRESET errors per peer IP and per deployment window, so a spike tied to a release can be attributed to a local shutdown change rather than the network.
Safe commands and checks
python -c "import socket,errno; s=socket.socket(); print('ECONNRESET',errno.ECONNRESET)"
python -c "import errno; print('WSAECONNRESET',getattr(errno,'WSAECONNRESET','n/a'))"
python -m py_compile <path_to_app_module>
python -c "import socket; help(socket.socket.close)" | head -n 40