Introduction
Most backend applications depend on services they do not control. Even a normally reliable API can become slow, return temporary errors, or stop responding.
A resilient HTTP client should set timeouts, classify failures, and retry only when doing so is safe. Retries without limits can make an outage worse by sending even more traffic to an already struggling service.
1. Configure request timeouts
Java’s built-in HttpClient supports connection timeouts and per-request timeouts:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/status"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
The connection timeout limits how long the client waits to establish a connection. The request timeout limits how long the request can take according to the client’s request handling.
Choose values based on the dependency’s expected latency and the overall deadline of the operation.
2. Classify failures before retrying
Not every failure is temporary. A connection reset may be transient, while a 400 Bad Request usually indicates that the request needs to change.
Consider these categories:
- Connection failures: may be transient.
- Timeouts: may justify a limited retry if the operation is safe.
- HTTP 429: respect the service’s rate-limit guidance, including Retry-After when provided.
- HTTP 5xx: may be temporary, but retries should still be bounded.
- HTTP 4xx: generally require correcting the request or permissions.
The correct policy depends on the API contract. Do not blindly retry every non-success status.
3. Use bounded exponential backoff
A common retry strategy increases the delay after each failure and adds jitter to avoid many clients retrying simultaneously.
For example, the base delay might grow from hundreds of milliseconds to a few seconds, subject to a maximum attempt count and total deadline.
Avoid writing a retry loop that can run indefinitely. Also avoid stacking multiple independent retry policies at several layers, since this can multiply the number of requests unexpectedly.
4. Be careful with non-idempotent operations
Retrying a GET request is usually easier to reason about than retrying a payment or order-creation request.
If the server completed a request but the response was lost, the client may not know whether the operation succeeded. Retrying could create a duplicate.
Use idempotency keys or an equivalent server-supported mechanism for operations where duplicate execution would be harmful. A client-side timeout does not prove that the server did nothing.
5. Add observability
Record request duration, status codes, timeout counts, retries, and final failures. Include a correlation identifier where appropriate, but avoid logging authorization headers or sensitive payloads.
Monitor retry rates alongside latency. A rise in retries can be an early sign of a dependency problem, even while the overall success rate appears acceptable.
Conclusion
Reliable HTTP clients combine sensible timeouts, bounded retries, backoff, and clear failure classification. Make retries safe for the operation, enforce an overall deadline, and monitor the behavior so resilience mechanisms do not amplify an outage.