Skip to content

Add optional HTTP/2 PING keepalive to prevent idle connection timeouts #1080

@balaji-g

Description

@balaji-g

Summary

httpcore's HTTP/2 implementation uses the h2 library for HTTP/2 framing but never sends PING frames. This causes connections to be silently killed by infrastructure components (load balancers, proxies, CDNs) that enforce idle timeouts on TCP connections.

HTTP/2 PING frames (RFC 9113 §6.7) exist precisely for this purpose — they generate application-layer traffic that resets idle timers. The h2 library already supports conn.ping(), but httpcore never calls it.

Problem

Many network intermediaries enforce idle timeouts on TCP connections. For example, AWS Global Accelerator enforces a 340-second idle timeout that cannot be configured. TCP keepalive packets are explicitly excluded from resetting this timer — only application-layer data counts.

When a client makes a long-running HTTP/2 request (e.g., a request where the server takes several minutes to produce a response), the connection is killed mid-flight:

httpcore.RemoteProtocolError: Server disconnected

This affects any httpx-based client making long-running requests through infrastructure with idle timeouts.

Reproduction

import httpx

# HTTP/2 is negotiated via ALPN, but no PING frames are ever sent.
# If the server takes longer than the intermediary's idle timeout,
# the connection is dropped.
client = httpx.Client(http2=True, timeout=httpx.Timeout(900.0))
resp = client.post("https://example.com/long-running-endpoint", json={...})
# Raises RemoteProtocolError at the idle timeout boundary

Evidence

I tested this against a server with a configurable response delay of 420s, behind an intermediary with a 340s idle timeout:

Client HTTP/2 PING frames Result
httpx (http2=True) Yes (via ALPN) None sent FAIL at 340sServer disconnected
httpx (http2=False) No (HTTP/1.1) N/A FAIL at 340sServer disconnected
Raw h2 library with conn.ping() every 60s Yes Sent every 60s PASS — 200 OK at 421s

The h2 library's ping() method works correctly — httpcore just never calls it.

Proposed change

Add an optional h2_ping_interval parameter to HTTP2Connection (and surface it through httpx.HTTPTransport / httpx.Client). When set, a background thread sends PING frames at the specified interval on active connections.

The implementation is small. In HTTP2Connection:

  1. Accept h2_ping_interval: float | None = None in __init__
  2. After _send_connection_init, start a daemon thread that calls self._h2_state.ping() + self._write_outgoing_data() every h2_ping_interval seconds
  3. Stop the thread in close()

Default would be None (current behavior, no PINGs), so this is fully backwards-compatible.

References

  • RFC 9113 §6.7 — PING: "PING frames are a mechanism for measuring a minimal round-trip time from the sender, as well as determining whether an idle connection is still functional."
  • AWS Global Accelerator idle timeout: 340s, not configurable, TCP keepalives excluded
  • The h2 library already supports H2Connection.ping() — httpcore just needs to call it

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions