Can Async HTTP Requests Really Boost Test Performance? A Practical Java NIO Study
This article explores the practical impact of asynchronous HTTP requests in Java NIO‑based performance testing, detailing server setup with Moco, baseline measurements, various async implementations, and the trade‑offs between raw speed gains and CPU consumption.
Background and Motivation
After learning the basics of Java NIO in a previous post, the author investigated whether an asynchronous HTTP client could improve performance testing efficiency. Initial expectations were that async requests would reduce latency, but real‑world tests showed high CPU usage when many threads were spawned.
Moco Server Setup
A simple Moco server is created to simulate a fixed‑delay HTTP endpoint. The Groovy script below starts the server on port 12345 and returns a JSON response after a configurable delay.
import com.mocofun.moco.MocoServer
class TestDemo extends MocoServer {
static void main(String[] args) {
def log = getServerNoLog(12345)
server.response(delay(jsonRes(getJson("Have=Fun ~ Tester !")), 10))
def run = run(log)
waitForKey("FunTester")
run.stop()
}
}Baseline Synchronous Test
The framework provides several request methods; a simple loop sends 200 requests sequentially and measures total time. The observed execution time ranges from 1250 ms to 1400 ms.
public static void main(String[] args) {
String url = "http://localhost:12345/FunTester";
HttpGet httpGet = getHttpGet(url);
LOG_KEY = false;
getHttpResponse(httpGet);
long start = Time.getTimeStamp();
for (int i = 0; i < 200; i++) {
getHttpResponse(httpGet);
}
long end = Time.getTimeStamp();
output(end - start);
testOver();
}Async Request Without Response Handling
By starting the async client and sending 100 requests without waiting for responses, the measured time drops to 18‑25 ms, suggesting a dramatic speedup. However, because responses are never read, this does not reflect true end‑to‑end latency.
public static void main(String[] args) {
String url = "http://localhost:12345/FunTester";
HttpGet httpGet = getHttpGet(url);
LOG_KEY = false;
ClientManage.startAsync();
getHttpResponse(httpGet);
long start = Time.getTimeStamp();
for (int i = 0; i < 100; i++) {
executeSync(httpGet);
}
long end = Time.getTimeStamp();
output(end - start);
testOver();
}Async Request With Response Processing (FutureCallback)
The Apache FutureCallback interface is implemented to log each response. The callback prints the response body or logs failures and cancellations.
public static final FutureCallback<HttpResponse> logCallback = new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse httpResponse) {
HttpEntity entity = httpResponse.getEntity();
String content = getContent(entity);
logger.info("Response:{}", content);
}
@Override
public void failed(Exception e) {
logger.warn("Response failed", e);
}
@Override
public void cancelled() {
logger.warn("Cancelled execution");
}
};Collecting all futures and blocking on each yields a total time of 110‑130 ms, roughly ten times faster than the synchronous baseline.
public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
String url = "http://localhost:12345/FunTester";
HttpGet httpGet = getHttpGet(url);
LOG_KEY = false;
ClientManage.startAsync();
getHttpResponse(httpGet);
List<Future<HttpResponse>> fs = new ArrayList<>();
long start = Time.getTimeStamp();
for (int i = 0; i < 100; i++) {
Future<HttpResponse> httpResponseFuture = executeSync(httpGet);
fs.add(httpResponseFuture);
}
for (Future<HttpResponse> f : fs) {
HttpResponse httpResponse = f.get(); // block
EntityUtils.consume(httpResponse.getEntity()); // release
}
long end = Time.getTimeStamp();
output(end - start);
testOver();
}Async Request With CountDownLatch
Using java.util.concurrent.CountDownLatch, each request invokes a custom FutureCallback that asserts the response content and counts down. After all callbacks complete, the total elapsed time is recorded.
public static void main(String[] args) {
String url = "http://localhost:12345/FunTester";
HttpGet httpGet = getHttpGet(url);
LOG_KEY = false;
ClientManage.startAsync();
getHttpResponse(httpGet);
CountDownLatch latch = new CountDownLatch(100);
long start = Time.getTimeStamp();
for (int i = 0; i < 100; i++) {
executeSync(httpGet, new FunTester(latch));
}
long end = Time.getTimeStamp();
output(end - start);
testOver();
}
private static class FunTester implements FutureCallback<HttpResponse> {
CountDownLatch latch;
FunTester(CountDownLatch latch) { this.latch = latch; }
@Override
public void completed(HttpResponse result) {
try {
HttpEntity entity = result.getEntity();
String content = getContent(entity);
Assert.assertTrue(content.contains("FunTes1ter"));
} finally { latch.countDown(); }
}
@Override public void failed(Exception ex) { logger.error(ex); }
@Override public void cancelled() { logger.error("Cancelled!"); }
}Conclusions
Purely fire‑and‑forget async calls can appear dramatically faster, but they ignore the cost of processing responses. When responses are collected—either via blocking futures or a latch—the async approach still yields a 5‑10× speedup over sequential calls, while consuming more CPU. The trade‑off between throughput and resource usage must be considered when integrating async HTTP clients into performance‑testing frameworks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
