Introduction
Jersey Client creates HTTP connections to call REST APIs, but when the underlying connection pool is exhausted, new requests fail with ConnectTimeoutException or ConnectionPoolTimeoutException: Timeout waiting for connection from pool. This happens when response entities are not fully consumed (preventing connection reuse), when the pool's max connections per route is too low for concurrent requests, or when a new Client instance is created per request instead of being reused. The default Apache HttpClient configuration in Jersey Client has only 2 connections per route, which is insufficient for most production workloads.
Symptoms
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:317)Or:
javax.ws.rs.ProcessingException: java.net.ConnectException: Connection refusedCommon Causes
- Response entity not consumed: Connection not returned to pool
- Creating Client per request: Each Client opens its own connections
- DefaultMaxPerRoute too low: Default of 2 connections per host
- Connection not closed: Response not closed after reading
- Idle connections not evicted: Stale connections in pool
- No connection timeout: Requests wait indefinitely for connections
Step-by-Step Fix
Step 1: Configure connection pool properly
```java import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; import org.glassfish.jersey.client.ClientConfig;
import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder;
public class HttpClientFactory {
public static Client createClient() { // Configure connection pool PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(200); // Total connections across all routes connManager.setDefaultMaxPerRoute(50); // Connections per host
// Configure request timeouts RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) // Connection establishment .setSocketTimeout(15000) // Data transfer .setConnectionRequestTimeout(10000) // Waiting for connection from pool .build();
CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connManager) .setDefaultRequestConfig(requestConfig) .evictIdleConnections(60, TimeUnit.SECONDS) .build();
ClientConfig config = new ClientConfig(); config.connectorProvider(new ApacheConnectorProvider()); config.property(ApacheClientProperties.CONNECTION_MANAGER, connManager);
return ClientBuilder.newBuilder() .withConfig(config) .build(); } }
// Create once and reuse private static final Client client = HttpClientFactory.createClient(); ```
Step 2: Always consume and close response
```java public String callApi(String url) { Response response = null; try { response = client.target(url) .request(MediaType.APPLICATION_JSON) .get();
if (response.getStatus() != 200) { // MUST still read and close the entity response.bufferEntity(); // Buffer for later reading throw new RuntimeException("API returned " + response.getStatus()); }
return response.readEntity(String.class); } finally { if (response != null) { response.close(); // Release connection back to pool } } } ```
Step 3: Monitor connection pool health
```java public void logPoolStats(PoolingHttpClientConnectionManager connManager) { PoolStats total = connManager.getTotalStats(); log.info("Connection pool - Leased: {}, Available: {}, Pending: {}", total.getLeased(), total.getAvailable(), total.getPending()); }
// Call periodically ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate( () -> logPoolStats(connManager), 0, 30, TimeUnit.SECONDS ); ```
Prevention
- Create Client once and reuse it across all requests
- Always close Response objects in finally blocks
- Set defaultMaxPerRoute based on expected concurrent requests per host
- Configure connectionRequestTimeout to fail fast when pool is exhausted
- Evict idle connections to prevent stale connection issues
- Monitor pool stats to detect connection leaks early
- Use try-with-resources for response handling when possible