AsyncHttpClient (AHC) is a high-performance, asynchronous HTTP client for Java built on top of Netty. It supports HTTP/1.1, HTTP/2, and WebSocket protocols.
ListenableFuture
and CompletableFutureResumableIOExceptionFilterJava 11+
Maven:
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>3.0.12</version>
</dependency>
Gradle:
implementation 'org.asynchttpclient:async-http-client:3.0.12'
For lower-latency I/O on Linux, add a native transport dependency:
<!-- Epoll (Linux) -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
<!-- io_uring (Linux) -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-io_uring</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
Then enable in config:
AsyncHttpClient client = asyncHttpClient(config().setUseNativeTransport(true));
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
<version>1.20.0</version>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>1.5.7-7</version>
</dependency>
Import the DSL helpers:
import static org.asynchttpclient.Dsl.*;
Create a client, execute a request, and read the response:
try (AsyncHttpClient client = asyncHttpClient()) {
// Asynchronous
client.prepareGet("https://www.example.com/")
.execute()
.toCompletableFuture()
.thenApply(Response::getResponseBody)
.thenAccept(System.out::println)
.join();
// Synchronous (blocking)
Response response = client.prepareGet("https://www.example.com/")
.execute()
.get();
}
Note:
AsyncHttpClientinstances are long-lived, shared resources. Always close them when done. Creating a new client per request will degrade performance due to repeated thread pool and connection pool creation.
Use config() to build an AsyncHttpClientConfig:
AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofSeconds(5))
.setRequestTimeout(Duration.ofSeconds(30))
.setMaxConnections(500)
.setMaxConnectionsPerHost(100)
.setFollowRedirect(true)
.setMaxRedirects(5)
.setCompressionEnforced(true));
Bound — build directly from the client:
Response response = client
.prepareGet("https://api.example.com/users")
.addHeader("Accept", "application/json")
.addQueryParam("page", "1")
.execute()
.get();
Unbound — build standalone via DSL, then execute:
Request request = get("https://api.example.com/users")
.addHeader("Accept", "application/json")
.addQueryParam("page", "1")
.build();
Response response = client.executeRequest(request).get();
Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE.
Use setBody to attach a body. Supported types:
| Type | Description |
|---|---|
String | Text content |
byte[] | Raw bytes |
ByteBuffer | NIO buffer |
InputStream | Streaming input |
File | File content |
Publisher<ByteBuf> | Reactive stream |
BodyGenerator | Custom body generation |
Response response = client
.preparePost("https://api.example.com/data")
.setHeader("Content-Type", "application/json")
.setBody("{\"name\": \"value\"}")
.execute()
.get();
For streaming bodies, see FeedableBodyGenerator which lets you push chunks
asynchronously.
Response response = client
.preparePost("https://api.example.com/upload")
.addBodyPart(new FilePart("file", new File("report.pdf"), "application/pdf"))
.addBodyPart(new StringPart("description", "Monthly report"))
.execute()
.get();
Part types: FilePart, ByteArrayPart, InputStreamPart, StringPart.
Response response = client.prepareGet("https://www.example.com/").execute().get();
Useful for debugging, but defeats the purpose of an async client in production.
execute() returns a ListenableFuture that supports completion listeners:
ListenableFuture<Response> future = client
.prepareGet("https://www.example.com/")
.execute();
future.addListener(() -> {
Response response = future.get();
System.out.println(response.getStatusCode());
}, executor);
If
executorisnull, the callback runs on the Netty I/O thread. Never block inside I/O thread callbacks.
client.prepareGet("https://www.example.com/")
.execute()
.toCompletableFuture()
.thenApply(Response::getResponseBody)
.thenAccept(System.out::println)
.join();
For most async use cases, extend AsyncCompletionHandler — it buffers the
full response and gives you a single onCompleted(Response) callback:
client.prepareGet("https://www.example.com/")
.execute(new AsyncCompletionHandler<String>() {
@Override
public String onCompleted(Response response) {
return response.getResponseBody();
}
});
For fine-grained control, implement AsyncHandler directly. This lets you
inspect status, headers, and body chunks as they arrive and abort early:
Future<Integer> future = client
.prepareGet("https://www.example.com/")
.execute(new AsyncHandler<>() {
private int status;
@Override
public State onStatusReceived(HttpResponseStatus s) {
status = s.getStatusCode();
return State.CONTINUE;
}
@Override
public State onHeadersReceived(HttpHeaders headers) {
return State.CONTINUE;
}
@Override
public State onBodyPartReceived(HttpResponseBodyPart part) {
return State.ABORT; // stop early — we only needed the status
}
@Override
public Integer onCompleted() {
return status;
}
@Override
public void onThrowable(Throwable t) {
t.printStackTrace();
}
});
HTTP/2 is enabled by default for HTTPS connections via ALPN negotiation. The client uses HTTP/2 when the server supports it and falls back to HTTP/1.1 otherwise. No additional configuration is required.
AsyncHttpClient client = asyncHttpClient(config()
.setHttp2MaxConcurrentStreams(100)
.setHttp2InitialWindowSize(65_535)
.setHttp2MaxFrameSize(16_384)
.setHttp2MaxHeaderListSize(8_192)
.setHttp2PingInterval(Duration.ofSeconds(30)) // keepalive pings
.setHttp2CleartextEnabled(true)); // h2c prior knowledge
When a handler suspends a response with ResponseBodyControl, the HTTP/2
per-stream window remains the buffering bound for that response. While at least
one response on a connection is suspended, AHC continues returning
connection-level credit so it cannot stall sibling streams. Connections with no
active suspension retain the normal 65,535-byte shared connection-window bound.
Once the last suspension ends, normal connection accounting resumes, although
credit already returned and data already queued cannot be revoked. Aggregate
queued response data during suspension can scale with the number of concurrent
streams. Use http2InitialWindowSize and http2MaxConcurrentStreams together
when an application needs a tighter aggregate bound.
To force HTTP/1.1, disable HTTP/2:
AsyncHttpClient client = asyncHttpClient(config().setHttp2Enabled(false));
WebSocket ws = client
.prepareGet("wss://echo.example.com/")
.execute(new WebSocketUpgradeHandler.Builder()
.addWebSocketListener(new WebSocketListener() {
@Override
public void onOpen(WebSocket ws) {
ws.sendTextFrame("Hello!");
}
@Override
public void onTextFrame(String payload, boolean finalFragment, int rsv) {
System.out.println(payload);
}
@Override
public void onClose(WebSocket ws, int code, String reason) {}
@Override
public void onError(Throwable t) { t.printStackTrace(); }
})
.build())
.get();
// Client-wide Basic auth
AsyncHttpClient client = asyncHttpClient(config()
.setRealm(basicAuthRealm("user", "password")));
// Per-request Digest auth
Response response = client
.prepareGet("https://api.example.com/protected")
.setRealm(digestAuthRealm("user", "password").build())
.execute()
.get();
// SCRAM-SHA-256 (RFC 7804)
Response response = client
.prepareGet("https://api.example.com/protected")
.setRealm(scramSha256AuthRealm("user", "password").build())
.execute()
.get();
Supported schemes: Basic, Digest, NTLM, SPNEGO/Kerberos, SCRAM-SHA-256.
// HTTP proxy
AsyncHttpClient client = asyncHttpClient(config()
.setProxyServer(proxyServer("proxy.example.com", 8080)));
// Authenticated proxy
AsyncHttpClient client = asyncHttpClient(config()
.setProxyServer(proxyServer("proxy.example.com", 8080)
.setRealm(basicAuthRealm("proxyUser", "proxyPassword"))));
SOCKS4 and SOCKS5 proxies are also supported.
(top 30 of 185)
Java
100.0%
AsyncHttpClient (AHC) is a high-performance, asynchronous HTTP client for Java built on top of Netty. It supports HTTP/1.1, HTTP/2, and WebSocket protocols.
ListenableFuture
and CompletableFutureResumableIOExceptionFilterJava 11+
Maven:
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>3.0.12</version>
</dependency>
Gradle:
implementation 'org.asynchttpclient:async-http-client:3.0.12'
For lower-latency I/O on Linux, add a native transport dependency:
<!-- Epoll (Linux) -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
<!-- io_uring (Linux) -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-io_uring</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
Then enable in config:
AsyncHttpClient client = asyncHttpClient(config().setUseNativeTransport(true));
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
<version>1.20.0</version>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>1.5.7-7</version>
</dependency>
Import the DSL helpers:
import static org.asynchttpclient.Dsl.*;
Create a client, execute a request, and read the response:
try (AsyncHttpClient client = asyncHttpClient()) {
// Asynchronous
client.prepareGet("https://www.example.com/")
.execute()
.toCompletableFuture()
.thenApply(Response::getResponseBody)
.thenAccept(System.out::println)
.join();
// Synchronous (blocking)
Response response = client.prepareGet("https://www.example.com/")
.execute()
.get();
}
Note:
AsyncHttpClientinstances are long-lived, shared resources. Always close them when done. Creating a new client per request will degrade performance due to repeated thread pool and connection pool creation.
Use config() to build an AsyncHttpClientConfig:
AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofSeconds(5))
.setRequestTimeout(Duration.ofSeconds(30))
.setMaxConnections(500)
.setMaxConnectionsPerHost(100)
.setFollowRedirect(true)
.setMaxRedirects(5)
.setCompressionEnforced(true));
Bound — build directly from the client:
Response response = client
.prepareGet("https://api.example.com/users")
.addHeader("Accept", "application/json")
.addQueryParam("page", "1")
.execute()
.get();
Unbound — build standalone via DSL, then execute:
Request request = get("https://api.example.com/users")
.addHeader("Accept", "application/json")
.addQueryParam("page", "1")
.build();
Response response = client.executeRequest(request).get();
Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE.
Use setBody to attach a body. Supported types:
| Type | Description |
|---|---|
String | Text content |
byte[] | Raw bytes |
ByteBuffer | NIO buffer |
InputStream | Streaming input |
File | File content |
Publisher<ByteBuf> | Reactive stream |
BodyGenerator | Custom body generation |
Response response = client
.preparePost("https://api.example.com/data")
.setHeader("Content-Type", "application/json")
.setBody("{\"name\": \"value\"}")
.execute()
.get();
For streaming bodies, see FeedableBodyGenerator which lets you push chunks
asynchronously.
Response response = client
.preparePost("https://api.example.com/upload")
.addBodyPart(new FilePart("file", new File("report.pdf"), "application/pdf"))
.addBodyPart(new StringPart("description", "Monthly report"))
.execute()
.get();
Part types: FilePart, ByteArrayPart, InputStreamPart, StringPart.
Response response = client.prepareGet("https://www.example.com/").execute().get();
Useful for debugging, but defeats the purpose of an async client in production.
execute() returns a ListenableFuture that supports completion listeners:
ListenableFuture<Response> future = client
.prepareGet("https://www.example.com/")
.execute();
future.addListener(() -> {
Response response = future.get();
System.out.println(response.getStatusCode());
}, executor);
If
executorisnull, the callback runs on the Netty I/O thread. Never block inside I/O thread callbacks.
client.prepareGet("https://www.example.com/")
.execute()
.toCompletableFuture()
.thenApply(Response::getResponseBody)
.thenAccept(System.out::println)
.join();
For most async use cases, extend AsyncCompletionHandler — it buffers the
full response and gives you a single onCompleted(Response) callback:
client.prepareGet("https://www.example.com/")
.execute(new AsyncCompletionHandler<String>() {
@Override
public String onCompleted(Response response) {
return response.getResponseBody();
}
});
For fine-grained control, implement AsyncHandler directly. This lets you
inspect status, headers, and body chunks as they arrive and abort early:
Future<Integer> future = client
.prepareGet("https://www.example.com/")
.execute(new AsyncHandler<>() {
private int status;
@Override
public State onStatusReceived(HttpResponseStatus s) {
status = s.getStatusCode();
return State.CONTINUE;
}
@Override
public State onHeadersReceived(HttpHeaders headers) {
return State.CONTINUE;
}
@Override
public State onBodyPartReceived(HttpResponseBodyPart part) {
return State.ABORT; // stop early — we only needed the status
}
@Override
public Integer onCompleted() {
return status;
}
@Override
public void onThrowable(Throwable t) {
t.printStackTrace();
}
});
HTTP/2 is enabled by default for HTTPS connections via ALPN negotiation. The client uses HTTP/2 when the server supports it and falls back to HTTP/1.1 otherwise. No additional configuration is required.
AsyncHttpClient client = asyncHttpClient(config()
.setHttp2MaxConcurrentStreams(100)
.setHttp2InitialWindowSize(65_535)
.setHttp2MaxFrameSize(16_384)
.setHttp2MaxHeaderListSize(8_192)
.setHttp2PingInterval(Duration.ofSeconds(30)) // keepalive pings
.setHttp2CleartextEnabled(true)); // h2c prior knowledge
When a handler suspends a response with ResponseBodyControl, the HTTP/2
per-stream window remains the buffering bound for that response. While at least
one response on a connection is suspended, AHC continues returning
connection-level credit so it cannot stall sibling streams. Connections with no
active suspension retain the normal 65,535-byte shared connection-window bound.
Once the last suspension ends, normal connection accounting resumes, although
credit already returned and data already queued cannot be revoked. Aggregate
queued response data during suspension can scale with the number of concurrent
streams. Use http2InitialWindowSize and http2MaxConcurrentStreams together
when an application needs a tighter aggregate bound.
To force HTTP/1.1, disable HTTP/2:
AsyncHttpClient client = asyncHttpClient(config().setHttp2Enabled(false));
WebSocket ws = client
.prepareGet("wss://echo.example.com/")
.execute(new WebSocketUpgradeHandler.Builder()
.addWebSocketListener(new WebSocketListener() {
@Override
public void onOpen(WebSocket ws) {
ws.sendTextFrame("Hello!");
}
@Override
public void onTextFrame(String payload, boolean finalFragment, int rsv) {
System.out.println(payload);
}
@Override
public void onClose(WebSocket ws, int code, String reason) {}
@Override
public void onError(Throwable t) { t.printStackTrace(); }
})
.build())
.get();
// Client-wide Basic auth
AsyncHttpClient client = asyncHttpClient(config()
.setRealm(basicAuthRealm("user", "password")));
// Per-request Digest auth
Response response = client
.prepareGet("https://api.example.com/protected")
.setRealm(digestAuthRealm("user", "password").build())
.execute()
.get();
// SCRAM-SHA-256 (RFC 7804)
Response response = client
.prepareGet("https://api.example.com/protected")
.setRealm(scramSha256AuthRealm("user", "password").build())
.execute()
.get();
Supported schemes: Basic, Digest, NTLM, SPNEGO/Kerberos, SCRAM-SHA-256.
// HTTP proxy
AsyncHttpClient client = asyncHttpClient(config()
.setProxyServer(proxyServer("proxy.example.com", 8080)));
// Authenticated proxy
AsyncHttpClient client = asyncHttpClient(config()
.setProxyServer(proxyServer("proxy.example.com", 8080)
.setRealm(basicAuthRealm("proxyUser", "proxyPassword"))));
SOCKS4 and SOCKS5 proxies are also supported.
(top 30 of 185)
Java
100.0%