Fixing ECONNRESET on a Self-Hosted HTTP Bridge

A calendar-sync bridge dropped connections under load with 'connection closed unexpectedly'. Four bridge tweaks later, the durable fix was async-ack plus onError handling in the client.

A self-hosted CalDAV bridge I run kept dropping its connection to an n8n client. The workflow reported read ECONNRESET — connection closed unexpectedly, always on a mid-batch item, and only under load. Worse, the bridge’s own logs showed every request returned 200: the data actually landed upstream, but the client still saw a failure.

The first fixes (and why they kept not quite working)

The bridge was a Python ThreadingHTTPServer that PUT events to a Yahoo CalDAV endpoint. Four changes were tried in sequence:

  • Single-threaded → threaded. Needed, so concurrent POSTs didn’t queue, but not sufficient.
  • HTTP/1.1 → HTTP/1.0. Kills keep-alive reuse of half-closed sockets. Helped, reduced the frequency.
  • Upstream retry loop. Absorbed transient Yahoo failures inside the bridge.
  • Explicit Connection: close + flush(). Forced a fresh connection per response.

After all four, the sync succeeded at 05:00 and 11:00, then errored again at 17:00 on a 40-event burst. The diagnostic that cracked it: the failing item’s execution time was ~15 seconds — exactly the bridge’s upstream timeout. The reset wasn’t a socket-reuse problem; it was n8n’s client timing out while the bridge thread was blocked on a slow Yahoo PUT.

The durable fix: stop blocking the connection

The real problem was that the bridge held the client connection open while it did a slow upstream write. Two changes fixed it for good:

  1. Async-ack. The bridge reads the request, responds 200 accepted immediately, then does the upstream PUT in a background thread. The client’s connection closes instantly; there’s no 15-second idle window to reset.
  2. Client-side onError. Set the n8n node’s onError to continueRegularOutput so a single dropped item no longer aborts the whole batch.

Retry settings were cosmetic here — retryOnFail: maxTries 3 does not retry ECONNRESET, because the run died in ~15s, long before the retry wait elapsed.

The lesson

When a proxy or bridge returns 200 for every request but the client still sees resets, the reset is happening on the client’s side of the bridge, not the upstream. Don’t keep patching socket options — decouple the slow upstream work from the client connection (async-ack) and make the client tolerate a single failure (onError). And watch the execution time: a ~15s failure that matches your upstream timeout is the signature of a blocking bridge, not a flaky socket.

Related

Filed under: Ops & Automation, Automation, n8n, Self-Hosting