Backend Development 11 min read

Optimizing Apache HttpClient for High-Concurrency Scenarios

This article explains how to improve a high‑traffic Java service by reusing a singleton HttpClient, enabling keep‑alive, configuring a pooling connection manager, and adding an idle‑connection monitor, which reduces average request latency from 250 ms to about 80 ms.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Optimizing Apache HttpClient for High-Concurrency Scenarios

Background: The service calls an HTTP‑based API with tens of millions of daily requests, originally using a new HttpClient for each request, resulting in an average latency of 250 ms.

Analysis: Identified three major overheads – repeated creation of HttpClient instances, repeated TCP handshakes, and unnecessary duplication of response entities.

Implementation:

1. Define a keep‑alive strategy to reuse connections.

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            HeaderElement he = it.nextElement();
            String param = he.getName();
            String value = he.getValue();
            if (value != null && param.equalsIgnoreCase("timeout")) {
                return Long.parseLong(value) * 1000;
            }
        }
        return 60 * 1000; // default 60 seconds
    }
};

2. Configure a PoolingHttpClientConnectionManager.

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(50); // adjust per business

3. Build a singleton HttpClient with the manager and keep‑alive strategy.

httpClient = HttpClients.custom()
    .setConnectionManager(connectionManager)
    .setKeepAliveStrategy(myStrategy)
    .setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build())
    .build();

4. Add an idle‑connection monitor thread to close expired and idle connections.

public static class IdleConnectionMonitorThread extends Thread {
    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;
    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) { this.connMgr = connMgr; }
    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    connMgr.closeExpiredConnections();
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) { }
    }
    public void shutdown() {
        shutdown = true;
        synchronized (this) { notifyAll(); }
    }
}

5. Use a ResponseHandler to automatically consume the entity and avoid manual stream handling.

public
T execute(final HttpHost target, final HttpRequest request,
        final ResponseHandler
responseHandler, final HttpContext context)
        throws IOException, ClientProtocolException {
    Args.notNull(responseHandler, "Response handler");
    final HttpResponse response = execute(target, request, context);
    final T result;
    try {
        result = responseHandler.handleResponse(response);
    } catch (final Exception t) {
        final HttpEntity entity = response.getEntity();
        try { EntityUtils.consume(entity); } catch (final Exception t2) { log.warn("Error consuming content after an exception.", t2); }
        if (t instanceof RuntimeException) throw (RuntimeException) t;
        if (t instanceof IOException) throw (IOException) t;
        throw new UndeclaredThrowableException(t);
    }
    final HttpEntity entity = response.getEntity();
    EntityUtils.consume(entity);
    return result;
}

Result: After applying these changes, the average request latency dropped from about 250 ms to roughly 80 ms, eliminating frequent thread‑exhaustion alarms.

Additional notes: configure connection and socket timeouts, optionally adjust Nginx keep‑alive settings, and include Maven dependency for HttpClient 4.5.6.

BackendJavaPerformanceOptimizationHttpClientConnectionPooling
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.