Introduction
The Jersey Client (JAX-RS client implementation) maintains a connection pool managed by the underlying connector (Apache Connector, JDK HttpUrlConnector, or Grizzly). When the pool is exhausted -- typically because connections are not released, the pool size is too small, or the remote server is slow -- new requests fail with java.net.ConnectException: Connection timed out or org.apache.http.conn.ConnectionPoolTimeoutException. Unlike connection refused errors, timeout errors indicate that the client is waiting for an available connection slot that never becomes free.
Symptoms
javax.ws.rs.ProcessingException: java.net.ConnectException: Connection timed out (Connection timed out)
at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:273)
at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:297)Or with Apache Connector:
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:316)Monitoring shows connection accumulation:
$ ss -tnp | grep :443 | grep jersey | wc -l
247Common Causes
- Creating a new Client per request: Each
ClientBuilder.newClient()creates its own connection pool - Response not closed:
Response.close()not called, keeping the connection checked out - Default pool size too small: Apache Connector default
maxTotal=20is insufficient - Connection not returned to pool on error: Exception path does not close the response
- Long-lived idle connections timed out by server: Server closes idle connections but client does not know
- No connection validation: Client uses stale connections that the server has already closed
Step-by-Step Fix
Step 1: Use a shared Client singleton
```java import org.glassfish.jersey.apache.connector.ApacheClientProperties; import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder;
public class ApiClientFactory {
private static final Client client;
static { // Configure connection pool PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // Total connections connectionManager.setDefaultMaxPerRoute(50); // Per-host connections
// Configure timeouts RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) // 5 seconds .setSocketTimeout(30000) // 30 seconds .setConnectionRequestTimeout(10000) // 10 seconds to get from pool .build();
CloseableHttpClient httpClient = org.apache.http.impl.client.HttpClientBuilder.create() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .evictIdleConnections(60, TimeUnit.SECONDS) .evictExpiredConnections() .build();
ClientConfig config = new ClientConfig(); config.connectorProvider(new ApacheConnectorProvider()); config.property(ApacheClientProperties.DISABLE_COOKIES, true); config.property(ApacheClientProperties.CLIENT_CONFIG, httpClient);
client = ClientBuilder.newClient(config); }
public static Client getClient() { return client; } } ```
Step 2: Always close responses
```java public UserProfile getUserProfile(String userId) { Response response = ApiClientFactory.getClient() .target("https://api.example.com") .path("/users/{id}") .resolveTemplate("id", userId) .request(MediaType.APPLICATION_JSON) .get();
try { if (response.getStatus() != 200) { throw new RuntimeException("API returned: " + response.getStatus()); } return response.readEntity(UserProfile.class); } finally { response.close(); // ALWAYS close the response } } ```
Or with try-with-resources:
public UserProfile getUserProfile(String userId) {
try (Response response = ApiClientFactory.getClient()
.target("https://api.example.com/users/" + userId)
.request(MediaType.APPLICATION_JSON)
.get()) {
return response.readEntity(UserProfile.class);
}
}Step 3: Monitor connection pool health
```java import org.apache.http.conn.routing.HttpRoute; import org.apache.http.pool.PoolStats;
public class ConnectionPoolMonitor {
private final PoolingHttpClientConnectionManager connectionManager;
public void logPoolStats() { PoolStats total = connectionManager.getTotalStats(); log.info("Connection pool - Leased: {}, Available: {}, Pending: {}", total.getLeased(), total.getAvailable(), total.getPending()); } } ```
Prevention
- Always use a shared
Clientsingleton -- never create one per request - Close every
Responsein afinallyblock or try-with-resources - Set
connectionRequestTimeoutlower thanconnectTimeoutto fail fast on pool exhaustion - Enable
evictIdleConnectionsto clean up connections closed by the server - Monitor pool stats (leased, available, pending) in production
- Use
MaxPerRouteto limit connections to any single host and prevent one slow service from consuming all connections