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

bash
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:

bash
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:

bash
$ ss -tnp | grep :443 | grep jersey | wc -l
247

Common 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=20 is 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:

java
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 Client singleton -- never create one per request
  • Close every Response in a finally block or try-with-resources
  • Set connectionRequestTimeout lower than connectTimeout to fail fast on pool exhaustion
  • Enable evictIdleConnections to clean up connections closed by the server
  • Monitor pool stats (leased, available, pending) in production
  • Use MaxPerRoute to limit connections to any single host and prevent one slow service from consuming all connections