diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ConnectionReuseDemo.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ConnectionReuseDemo.java
new file mode 100644
index 0000000000..cbc9405ca8
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ConnectionReuseDemo.java
@@ -0,0 +1,220 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.examples;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.config.TlsConfig;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.nio.DefaultManagedAsyncClientConnection;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.nio.command.StaleCheckCommand;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.hc.core5.util.VersionInfo;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
+
+import java.net.URI;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.logging.log4j.Level.DEBUG;
+import static org.apache.logging.log4j.Level.INFO;
+import static org.apache.logging.log4j.Level.WARN;
+
+/**
+ * This example demonstrates connection reuse, with a specific focus on what happens when there are not enough requests
+ * in flight to keep all the connections active. There are several ways to configure a connection pool (see
+ * {@link PoolReusePolicy}, {@link PoolConcurrencyPolicy}, and
+ * {@link HttpAsyncClientBuilder#evictIdleConnections(TimeValue)}), and there are also numerous settings that affect
+ * connection expiry, including:
+ *
+ * - {@link ConnectionConfig#getTimeToLive()}
+ * - {@link ConnectionConfig#getIdleTimeout()}
+ * - {@link ConnectionConfig#getValidateAfterInactivity()}
+ * - {@link RequestConfig#getConnectionKeepAlive()}
+ *
+ * This example can be used to experiment with different config values in order to answer questions like:
+ *
+ * - Which connections get reused? Which ones expire?
+ * - Where are the various connection expiry settings implemented?
+ * - Do expired connections get leased out? If so, what happens?
+ * - When the connection pool is too large, does it shrink? If so, what size does it converge on?
+ * - Does inactive connection validation through {@link StaleCheckCommand} add latency?
+ *
+ */
+public class ConnectionReuseDemo {
+ private static final Logger LOG;
+
+ static {
+ final ConfigurationBuilder config = ConfigurationBuilderFactory.newConfigurationBuilder();
+ config.setStatusLevel(WARN);
+ config.setConfigurationName("ConnectionReuseDemo");
+
+ final AppenderComponentBuilder console = config.newAppender("APPLICATION", "CONSOLE")
+ .add(config.newLayout("PatternLayout")
+ .addAttribute("pattern", "%d{HH:mm:ss.SSS} %highlight{[%p]} (%t) %C{1}: %m%n"));
+
+ config.add(console)
+ .add(config.newRootLogger(INFO).add(config.newAppenderRef("APPLICATION")))
+ .add(config.newLogger(DefaultManagedAsyncClientConnection.class.getName(), DEBUG));
+
+ Configurator.initialize(config.build()).start();
+
+ LOG = LogManager.getLogger(ConnectionReuseDemo.class);
+ }
+
+ public static void main(final String[] args) throws InterruptedException {
+ final ClassLoader cl = ConnectionReuseDemo.class.getClassLoader();
+ LOG.info("Running client {}, core {}",
+ VersionInfo.loadVersionInfo("org.apache.hc.client5", cl).getRelease(),
+ VersionInfo.loadVersionInfo("org.apache.hc.core5", cl).getRelease());
+
+ final PoolConcurrencyPolicy concurrencyPolicy = PoolConcurrencyPolicy.OFFLOCK;
+ final PoolReusePolicy reusePolicy = PoolReusePolicy.FIFO;
+ final Timeout idleTimeout = null;
+ final TimeValue timeToLive = null;
+ final TimeValue validateAfterInactivity = TimeValue.ofSeconds(2);
+ final TimeValue evictIdleConnections = null;
+ final TimeValue connectionKeepAlive = TimeValue.ofSeconds(5);
+
+ LOG.info("Pool type: {} ({})", concurrencyPolicy, reusePolicy);
+ LOG.info("Connection config: idleTimeout={}, timeToLive={}, validateAfterInactivity={}",
+ idleTimeout, timeToLive, validateAfterInactivity);
+ LOG.info("evictIdleConnections: {}", evictIdleConnections);
+ LOG.info("connectionKeepAlive: {}", connectionKeepAlive);
+
+ final PoolingAsyncClientConnectionManager mgr = PoolingAsyncClientConnectionManagerBuilder.create()
+ .setMaxConnPerRoute(Integer.MAX_VALUE)
+ .setMaxConnTotal(Integer.MAX_VALUE)
+ .setConnPoolPolicy(reusePolicy)
+ .setPoolConcurrencyPolicy(concurrencyPolicy)
+ .setDefaultTlsConfig(TlsConfig.custom()
+ .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)
+ .build())
+ .setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setConnectTimeout(60, SECONDS)
+ .setSocketTimeout(60, SECONDS)
+ .setTimeToLive(timeToLive)
+ .setIdleTimeout(idleTimeout)
+ .setValidateAfterInactivity(validateAfterInactivity)
+ .build())
+ .build();
+
+ final CloseableHttpAsyncClient client = getBuilder(evictIdleConnections)
+ .disableAutomaticRetries()
+ .setConnectionManager(mgr)
+ .setDefaultRequestConfig(RequestConfig.custom()
+ .setConnectionKeepAlive(connectionKeepAlive)
+ .build())
+ .build();
+
+ client.start();
+
+ LOG.info("Sending warmup request");
+ join(call(client));
+ final HttpRoute route = mgr.getRoutes().iterator().next();
+ mgr.getStats(route);
+
+ LOG.info("Expanding connection pool");
+ IntStream.range(0, 10)
+ .mapToObj(unused -> call(client))
+ .collect(Collectors.toList())
+ .forEach(ConnectionReuseDemo::join);
+
+ LOG.info("{} connections available. Walking connection pool...", mgr.getStats(route).getAvailable());
+ for (int i = 0; i < 10; i++) {
+ Thread.sleep(1_000);
+ LOG.info("Sending request {}; {} connections available", i + 1, mgr.getStats(route).getAvailable());
+ final long startTime = System.nanoTime();
+ join(call(client));
+ LOG.info("Request took {} ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
+ }
+
+ LOG.info("Waiting for all connections to expire");
+ Thread.sleep(6_000);
+
+ LOG.info("Sending one last request (should establish a new connection)");
+ final int before = mgr.getStats(route).getAvailable();
+ join(call(client));
+ LOG.info("Connections available: {} -> {}", before, mgr.getStats(route).getAvailable());
+ }
+
+ private static HttpAsyncClientBuilder getBuilder(final TimeValue evictIdleConnections) {
+ if (evictIdleConnections != null) {
+ return HttpAsyncClientBuilder.create().evictIdleConnections(evictIdleConnections);
+ }
+ return HttpAsyncClientBuilder.create();
+ }
+
+ private static void join(final Future f) {
+ try {
+ f.get();
+ } catch (final Throwable ignore) {
+ }
+ }
+
+ private static Future call(final CloseableHttpAsyncClient client) {
+ final SimpleHttpRequest req = SimpleHttpRequest.create(Method.GET, URI.create("https://www.amazon.co.jp/"));
+ return client.execute(req,
+ new FutureCallback() {
+ @Override
+ public void completed(final SimpleHttpResponse result) {
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ LOG.error("Request failed", ex);
+ }
+
+ @Override
+ public void cancelled() {
+ LOG.error("Request cancelled");
+ }
+ });
+ }
+}