Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java HTTP retry project: bounded attempts and one wait budget

Last updated: 29 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

An HTTP retry repeats a selected operation after a classified failure, so both replay safety and the total attempt budget belong to the client contract.

Download Java source kit

Java 11+. The program uses JDK classes and requires no preview flags.

Retry a repeatable operation

The local server returns 503 on its first GET and 200 on its next GET. The client retries this read-only operation at most three times under one monotonic wait budget. A timeout or status does not automatically justify replaying a payment or another state-changing request.

Each attempt receives only the remaining budget. Starting a fresh full timeout for every attempt would let a request exceed its intended total wait. The program checks for exhaustion before sending and preserves interruption instead of treating it as another transient server failure.

This model retries a classified status only. It does not add random backoff, honor Retry-After or coordinate a shared rate limit. Those production policies need a representative load and service contract; a local two-response fixture cannot select them for every API.

Observe status before parsing

A received 503 is an HTTP response, not necessarily a transport exception. The client checks the status before treating the body as a quote. A response body parser also needs its own size and schema limits; the tiny synthetic body here is not an untrusted-data policy.

Working program

Java
import com.sun.net.httpserver.HttpServer;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class RetriedLocalQuote {
    static String fetch(HttpClient client,URI uri,Duration budget)throws Exception{
        long deadline=System.nanoTime()+budget.toNanos();
        for(int attempt=0;attempt<3;attempt++){
            long remaining=deadline-System.nanoTime();if(remaining<=0)throw new TimeoutException("Quote budget");
            HttpRequest request=HttpRequest.newBuilder(uri).timeout(Duration.ofNanos(remaining)).GET().build();
            HttpResponse<String> response=client.send(request,HttpResponse.BodyHandlers.ofString());
            if(response.statusCode()==200)return response.body();if(response.statusCode()!=503)throw new IllegalStateException("Unexpected HTTP status");
        }throw new IllegalStateException("Attempt limit");
    }
    public static void main(String[] args)throws Exception{
        AtomicInteger calls=new AtomicInteger();HttpServer server=HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(),0),0);ExecutorService handlers=Executors.newSingleThreadExecutor();server.setExecutor(handlers);
        server.createContext("/quote",exchange->{int attempt=calls.incrementAndGet();byte[] body=(attempt==1?"busy":"quote=150").getBytes(StandardCharsets.UTF_8);exchange.sendResponseHeaders(attempt==1?503:200,body.length);try(java.io.OutputStream output=exchange.getResponseBody()){output.write(body);}});server.start();
        try{
            URI uri=new URI("http",null,server.getAddress().getAddress().getHostAddress(),server.getAddress().getPort(),"/quote",null,null);
            HttpClient client=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(1)).build();System.out.println(fetch(client,uri,Duration.ofSeconds(2)));System.out.println("attempts="+calls.get());
        }finally{server.stop(0);handlers.shutdownNow();if(!handlers.awaitTermination(2,TimeUnit.SECONDS))throw new IllegalStateException("Server worker");}
    }
}

Output

Output
quote=150
attempts=2

Costs and boundaries

At most three sequential attempts are admitted, each with a remaining request timeout. Cleanup has a separate bounded worker wait. This fixture does not establish an end-to-end deadline for arbitrary DNS, response processing or external side effects, and it is not a server load test.

Common Mistakes

  • A failed response is not proof that a state-changing operation never took effect.
  • Do not reset the entire budget for each retry.
  • Interruption is not a server status to retry blindly.

Read next

Local response handling, Shared deadlines.

Apply this contract in Spring

Spring Boot receipt API project: build, test and inspect the limits. These lessons keep framework assembly separate from the Java contract.

java
http-failure-project
Storage details