Skip to content

fix(storage): reuse keep-alive connections in webhook storage - #223

Merged
howethomas merged 1 commit into
mainfrom
webhook-keepalive
Oct 11, 2026
Merged

howethomas merged 1 commit into
mainfrom
webhook-keepalive

Conversation

@howethomas

Copy link
Copy Markdown
Contributor

Why

Webhook storage called requests.post once per write, opening a new TCP connection each time and leaving it in TIME_WAIT. On a busy deployment (a few hundred writes/s from one conserver pod) the pod ran out of ephemeral ports. Every write then failed with Failed to establish a new connection and went to the storage DLQ: hundreds of thousands of vCons in under half an hour.

What

  • One requests.Session per worker process, shared by the short-lived storage threads (a new ThreadPoolExecutor per vCon, so a thread-local session would never be reused). urllib3's pool is thread-safe, and these POSTs carry no cookies.
  • HTTPAdapter pool sized by a new pool-size option (default 16; size it to CONSERVER_VCON_CONCURRENCY).
  • The session is rebuilt when the pid changes, so a forked worker never shares its parent's sockets.
  • Behaviour is otherwise unchanged: same timeout, headers, metrics, and raise_for_status() DLQ propagation.

Tests

  • New common/tests/test_webhook_keepalive.py: a local HTTP/1.1 receiver sees five sequential writes arrive on one client port; a changed pid gets a fresh session.
  • Mutation-checked: reverting to bare requests.post fails the first test; dropping the pid check fails the second.
  • The existing test_webhook_storage_failures.py is updated to patch the session instead of requests.post. All 7 pass.

🤖 Generated with Claude Code

Each webhook write called requests.post, which opens a new TCP connection
and leaves it in TIME_WAIT. At a few hundred writes/s a single conserver
pod ran out of ephemeral ports and every write failed with "Failed to
establish a new connection", sending the vCons to the storage DLQ.

Use one requests.Session per worker process, shared across the
short-lived storage threads, with an HTTPAdapter pool sized by the new
"pool-size" option (default 16). The session is rebuilt when the pid
changes so a forked worker never shares its parent's sockets.

Tests: a local HTTP/1.1 receiver sees five sequential writes arrive on
one client port; a forked pid gets a fresh session. Both fail when the
change is reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@howethomas
howethomas merged commit a477e0b into main Oct 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant