Connection management

HttpClient reuses persistent connections to reduce latency and resource usage. Connection management is handled by dedicated connection managers for classic (blocking) and async I/O.

Connection managers

The main connection managers are:

  • PoolingHttpClientConnectionManager (classic I/O)
  • PoolingAsyncClientConnectionManager (async I/O)

Both managers:

  • Maintain per-route and total connection limits.
  • Reuse persistent connections when possible.
  • Enforce connection time-to-live (TTL) and idle expiry.
  • Provide APIs to close idle or expired connections explicitly.

For a detailed description of pooling policies see Connection pooling.

Per-route and total limits

Connection limits are expressed in terms of:

  • Per-route limit – maximum number of connections for a single HttpRoute.
  • Total limit – maximum number of connections across all routes.

Classic:

final PoolingHttpClientConnectionManager cm =
        PoolingHttpClientConnectionManagerBuilder.create()
                .setMaxConnTotal(200)
                .setMaxConnPerRoute(50)
                .build();

Async:

final PoolingAsyncClientConnectionManager cm =
        PoolingAsyncClientConnectionManagerBuilder.create()
                .setMaxConnTotal(200)
                .setMaxConnPerRoute(50)
                .build();

These limits should reflect expected concurrency and the capacity of the remote services and network.

Connection lifetime and idle timeout

Connection lifetime (TTL) limits how long a persistent connection can be reused regardless of its keep-alive semantics. Idle timeout limits how long a connection may stay idle in the pool before being considered stale.

Both are configured via ConnectionConfig:

final ConnectionConfig connectionConfig = ConnectionConfig.custom()
        .setTimeToLive(TimeValue.ofMinutes(5))
        .setIdleTimeout(TimeValue.ofMinutes(1))
        .build();

Classic:

final PoolingHttpClientConnectionManager cm =
        PoolingHttpClientConnectionManagerBuilder.create()
                .setDefaultConnectionConfig(connectionConfig)
                .build();

Async:

final PoolingAsyncClientConnectionManager cm =
        PoolingAsyncClientConnectionManagerBuilder.create()
                .setDefaultConnectionConfig(connectionConfig)
                .build();

Connections that exceed their TTL or idle timeout are discarded when leased or by explicit eviction.

Evicting idle and expired connections

Applications are encouraged to run a background task to evict idle and expired connections from the pool.

Classic:

cm.closeExpired();
cm.closeIdle(TimeValue.ofMinutes(1));

Async:

asyncManager.closeExpired();
asyncManager.closeIdle(TimeValue.ofMinutes(1));

This keeps the pool clean in long-lived applications and reduces the probability of using dead connections after long periods of inactivity.

Validation after inactivity

Persistent connections kept idle for a long time can become half-closed by intermediaries (stale connections). HttpClient can validate such connections before re-use based on a configurable inactivity threshold:

final ConnectionConfig connectionConfig = ConnectionConfig.custom()
        .setValidateAfterInactivity(TimeValue.ofSeconds(2))
        .build();

When the inactivity period of a connection exceeds this threshold, the manager will perform a lightweight isStale() check before leasing it.

  • A small positive value (e.g. 1–2 seconds) is often a good compromise.
  • A non-positive value disables validation and may increase the risk of encountering stale connections on the first request after a long idle period.

Classic and async managers both honour validateAfterInactivity when leasing connections.

Concurrency policies and off-lock disposal

Connection managers support different pool concurrency policies via PoolConcurrencyPolicy:

  • STRICT – per-route queues and strong fairness (default).
  • LAX – relaxed fairness in favour of higher throughput.
  • OFFLOCK – experimental route-segmented policy reducing contention on shared structures.

See Connection pooling for details and examples.

Blocking connection pools can optionally move slow graceful closes off the hot pool locks:

final PoolingHttpClientConnectionManager cm =
        PoolingHttpClientConnectionManagerBuilder.create()
                .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
                .setOffLockDisposalEnabled(true)
                .build();

With off-lock disposal enabled, removal from the pool happens immediately, while the actual graceful close is performed outside the core pool synchronisation.

Practical recommendations

  • Start with the default STRICT policy and conservative limits.
  • Size maxConnTotal and maxConnPerRoute based on expected concurrency and backend capacity; monitor and adjust.
  • Always run periodic eviction of idle and expired connections in long-lived applications.
  • Enable validateAfterInactivity with a small positive value if connections are kept idle for extended periods.
  • Consider LAX or OFFLOCK (experimental) for highly concurrent workloads where reducing contention is more important than strict per-route fairness.