Skip to content

fix: use persistent httpx.Client to eliminate per-call TCP/TLS overhead#36

Open
sameer-ahmed wants to merge 2 commits into
runekaagaard:mainfrom
sameer-ahmed:fix/persistent-http-client
Open

fix: use persistent httpx.Client to eliminate per-call TCP/TLS overhead#36
sameer-ahmed wants to merge 2 commits into
runekaagaard:mainfrom
sameer-ahmed:fix/persistent-http-client

Conversation

@sameer-ahmed
Copy link
Copy Markdown

Problem

httpx.request() (the top-level convenience function) creates a brand new TCP connection and TLS handshake on every single API call. On internal/corporate Redmine servers this adds ~2 minutes of overhead per request, making bulk operations extremely slow.

We measured this empirically:

  • Fetch 1 ticket: ~2 min 3 sec
  • Update 1 ticket: ~2 min 6 sec
  • 10 calls total (5 fetch + 5 update) = ~20 minutes for what should take seconds

Fix

Replace httpx.request() with a module-level httpx.Client instance that reuses the underlying TCP/TLS connection across calls. After the first connection is established, subsequent calls reuse it — reducing latency to milliseconds.

# Before: new connection every call
response = httpx.request(method=..., verify=..., timeout=...)

# After: persistent client, connection reused
_http_client = httpx.Client(timeout=60.0, verify=not REDMINE_DANGEROUSLY_ACCEPT_INVALID_CERTS)
response = _http_client.request(method=...)

Why this happens

httpx.request() is a convenience wrapper that internally does:

with httpx.Client(...) as client:
    return client.request(...)

The with block closes the client (and its connection pool) after every call. The persistent client avoids this entirely.

This is consistent with the httpx docs recommendation — use a Client instance for multiple requests rather than the top-level API.

Impact

  • Fixes severe latency on corporate/internal Redmine instances (HTTPS with self-signed certs or slow DNS)
  • No behaviour change for public Redmine instances (just faster)
  • Zero API surface change

Using httpx.request() (the top-level convenience function) creates a brand
new TCP connection and TLS handshake on every single API call. On internal
or corporate Redmine servers this can add ~2 minutes of overhead per request,
making bulk operations extremely slow.

Replace with a module-level httpx.Client instance that reuses the underlying
connection across calls, reducing latency to milliseconds after the first
connection is established.
@sameer-ahmed sameer-ahmed force-pushed the fix/persistent-http-client branch from f39694d to 4adf9df Compare April 8, 2026 05:59
httpx default keepalive_expiry is 5s, causing a full ~600ms TLS
reconnect on every call after a short pause. Set keepalive_expiry=120s
and max_keepalive_connections=5 to reuse connections across calls.
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