Skip to content

Add opt-in connection pool with lease semantics to AWSCRTHTTPClient #701

@bilardi

Description

@bilardi

Summary

AWSCRTHTTPClient._get_connection() (packages/smithy-http/src/smithy_http/aio/crt.py:224) keeps a single shared HTTP connection per (scheme, host, port). The existing TODO at that location (# TODO: Use CRT connection pooling instead of this basic kind) acknowledges the limitation but is currently untracked.

In addition, the deepcopy(self._config) performed by every smithy-generated operation invokes AWSCRTHTTPClient.__deepcopy__, which returns a fresh instance with an empty _connections cache (the risk was flagged in PR #355: "I'm a bit concerned about not sharing the connection pool"). The effect is measurable: every bidirectional operation pays a full TLS + ALPN + HTTP/2 cold start, adding ~60 ms to median latency vs an implementation that reuses connections across operations.

This issue proposes an opt-in connection pool with lease semantics, attached to _AWSCRTEventLoop so it survives the deepcopy. The pool solves the per-connection stream limit for bidirectional streaming (Polly, Transcribe, Bedrock Nova Sonic) without breaking any existing API.

Empirical impact

Measured on a realtime fan-out workload (1 speaker -> 2 concurrent listener languages, Polly bidirectional streaming, us-west-2 region):

Implementation P50 Sample size
aws-sdk-polly 0.6.0 vanilla 350 ms 30 (3 run)
aws-sdk-polly 0.6.0 + this proposal 290 ms 40 (4 run)

The PR recovers ~60 ms of median latency by enabling warm connection reuse across operations. Full historical comparison across 4 benchmarks (including the reference hand-rolled package) and reproduction commands are in PR_TO_SMITHY.md § Full evolution table.

Proposed design

Opt-in via a new ConnectionPoolConfig dataclass on AWSCRTHTTPClientConfig. Default None preserves current behavior; setting the field activates a bounded pool with lease semantics per (scheme, host, port).

from smithy_http.aio.crt import AWSCRTHTTPClient, AWSCRTHTTPClientConfig, ConnectionPoolConfig

# Default: unchanged behavior, no pool
transport = AWSCRTHTTPClient()

# Opt-in with default cap (8 connections per host)
transport = AWSCRTHTTPClient(
    client_config=AWSCRTHTTPClientConfig(
        connection_pool=ConnectionPoolConfig()
    )
)

The ConnectionPool is attached lazily to _AWSCRTEventLoop.pool on first send when connection_pool is set. The eventloop is already shared by __deepcopy__ of AWSCRTHTTPClient, so the pool survives the deepcopy. Lease ownership transfers to AWSCRTHTTPResponse and is released in chunks()'s finally block, tying lease lifetime to stream lifetime. Full mechanism analysis: PR_TO_SMITHY.md § Detailed mechanism.

Implementation

Branch: bilardi/smithy-python @ add-awscrt-connection-pool

Single file modified: packages/smithy-http/src/smithy_http/aio/crt.py, +248 / -24 lines.

Quality checks: make check-py clean (ruff + ruff format + pyright strict mode, 0 errors 0 warnings, Python 3.12 target).

Backwards compatible: connection_pool default None, existing _send_via_cache path untouched semantically. New public symbols: ConnectionPoolConfig and ConnectionPool (the _Lease helper is private).

Use cases affected

Open questions before sending the PR

  1. Default max_connections_per_host: chosen 8 based on the amazon-polly-streaming reference implementation validated in production with multilingual fan-out workloads. Open to a different default.

  2. Stale connection detection beyond is_open(): 2 of 4 measurement runs showed a mid-run latency spike. Three candidate causes: Polly server-side jitter, stale-connection in the pool, network spike. Plan to instrument connection_age_ms to disambiguate; can address before merge or as a separate issue.

  3. Tests in upstream style: packages/smithy-http/tests/unit/aio/ doesn't currently include CRT tests. Pure-unit tests for ConnectionPool logic can be written without CRT by mocking the connection factory. Prefer unit, integration, or both?

  4. Naming and module structure: file already exports ConnectionPoolKey / ConnectionPoolDict aliases. Adding ConnectionPoolConfig / ConnectionPool / _Lease to the same module is consistent. Open to extract to a separate _pool.py module if preferred.

References

License: contribution under Apache-2.0, consistent with the repository.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions