keepcalculator

06 Numbers and Powers

Java REST Client Reliability: Retries, Pagination, Errors

Card No. 06·33130 · Updated · Standard formula, shown below

Worst-case wait (seconds) 3.50 s
Timeout (seconds)
10 s
You entered
Max retries
3
You entered
Base backoff (ms)
500 ms
You entered
You enter
Timeout (seconds), Max retries, Base backoff (ms)
You get
Worst-case wait (seconds)
Rounding
2 dp

To make your Java REST client reliable, you need to handle three things explicitly: retries with exponential backoff and jitter, pagination that follows the API’s cursor or page convention, and structured error handling that distinguishes transient from permanent failures. This article explains how to implement each in plain Java, using standard libraries like java.net.http.HttpClient and optional resilience libraries, without relying on any specific framework.

Formula

The worst-case total wait time for exponential backoff with r retries and base backoff b milliseconds is:

total_wait_seconds = (b / 1000) * (2^r - 1)

Variables:

  • r – maximum number of retry attempts (integer ≥ 0)
  • b – base backoff in milliseconds (the delay after the first failure)
  • total_wait – sum of all delays if every retry fails, in seconds

This assumes the delay doubles after each failure: b, 2b, 4b, … up to 2^(r-1)*b. The sum is b * (2^r - 1). In practice you add jitter (randomness) to avoid thundering herds, so the actual wait varies.

Worked Example

Using the defaults from the calculator: r = 3, b = 500 ms.

  1. First retry delay: 500 ms
  2. Second retry delay: 1000 ms
  3. Third retry delay: 2000 ms
  4. Total worst-case wait: 500 + 1000 + 2000 = 3500 ms = 3.5 seconds

The formula gives (500/1000) * (2^3 - 1) = 0.5 * 7 = 3.5 seconds, matching the calculator output.

Why Retries Fail Without Strategy

Naive retry loops that immediately re-send a failed request often make things worse. If the server is overloaded, immediate retries pile on more load. If the failure is a 4xx client error (like 404 or 400), retrying will never succeed. A reliable client must:

  • Retry only on transient errors: network timeouts, 5xx responses, and specific 429 (Too Many Requests) with a Retry-After header.
  • Use exponential backoff: wait b, then 2b, then 4b, etc.
  • Add jitter: randomize the delay to prevent synchronized retry storms.
  • Respect the server’s Retry-After header when present.
  • Cap the number of retries to avoid infinite loops.

Without these, a simple for loop that retries 5 times immediately can turn a minor blip into a self-inflicted outage.

Pagination: The Two Main Styles

APIs paginate large result sets in two common ways:

  • Offset/limit: client sends ?offset=100&limit=50. Simple, but inefficient for large datasets because the server must skip rows.
  • Cursor-based: server returns an opaque token (e.g., next_cursor) in the response. The client sends that token in the next request. This is stable even when new items are added.

For cursor-based APIs, you must loop until the response indicates no more pages. A common pattern:

  1. Send initial request with no cursor.
  2. Extract next_cursor from the JSON response.
  3. If the cursor is null or empty, stop. Otherwise, send the next request with that cursor.
  4. Accumulate items from each page.

Be careful: some APIs use a boolean has_more field, others rely on an empty cursor. Read the API documentation and handle both if unsure.

Error Handling That Actually Helps

Robust error handling means mapping HTTP status codes and exceptions to meaningful outcomes. A practical approach:

  • 2xx: success – parse body.
  • 4xx: permanent client error – do not retry. Log the request details (without sensitive data) and surface a clear message.
  • 429: rate limited – retry after the Retry-After header, or use exponential backoff if the header is missing.
  • 5xx: server error – retry with backoff, but only up to the max retries.
  • IOException / timeout: transient – retry.

In Java, you can wrap the HTTP call in a try-catch and inspect the HttpResponse status code. For timeouts, set a generous connect and read timeout on the HttpClient (e.g., 10 seconds) – a hung connection is worse than a quick failure.

Building a Reliable Client Step by Step

Here is a minimal, framework-free implementation outline using java.net.http.HttpClient:

  1. Create an HttpClient with a connect timeout and a reasonable read timeout.
  2. Write a method that sends a request and returns the response body as a string.
  3. In that method, loop up to maxRetries times.
  4. On each catch of IOException or InterruptedException, or a 5xx/429 status, compute backoff delay b * 2^attempt and sleep (with jitter).
  5. On 4xx (except 429), break and return the error.
  6. For pagination, call the send method repeatedly, extracting the cursor each time.

This pattern works for any REST API and avoids pulling in heavy dependencies. If you prefer a library, Resilience4j or Spring Retry offer ready-made retry decorators, but the core logic remains the same.

Common Mistakes and Misconceptions

  • Retrying all errors: Retrying a 400 Bad Request is pointless and can cause duplicate data on the server.
  • No jitter: Without jitter, all clients retry at the same time, recreating the original load.
  • Infinite pagination loop: If the server returns the same cursor forever, your loop never ends. Always add a max page count.
  • Ignoring the Retry-After header: Some APIs explicitly tell you how long to wait; ignoring it can get you banned.
  • Not closing the response body: In HttpClient, if you use BodyHandlers.ofString(), the body is read fully, but if you stream, you must close the stream to free resources.

Frequently Asked Questions

Should I retry on 401 Unauthorized?

No. A 401 means your credentials are wrong or expired. Retrying won’t fix it. Instead, refresh the token if you have a refresh mechanism, then retry once with the new token. But do not loop.

How do I handle pagination if the API doesn’t give a next cursor?

If the API uses offset/limit, you can increment the offset by the limit until you get fewer items than the limit. For cursor-based APIs, if the cursor is missing, treat it as the last page. Always check the API documentation for the exact field name.

What is the difference between exponential backoff and linear backoff?

Exponential backoff doubles the wait after each attempt (e.g., 1s, 2s, 4s), which is more aggressive in reducing load. Linear backoff waits the same time each attempt (e.g., 1s, 1s, 1s). Exponential is preferred for transient server overload because it gives the server more time to recover.

Can I use CompletableFuture for retries?

Yes. You can chain thenApply or handle to catch failures and schedule a retry with CompletableFuture.delayedExecutor(). This makes the retry non-blocking. The logic is the same, but you avoid sleeping on a thread.

How many retries is too many?

It depends on the API and your use case. For most public APIs, 3-5 retries with exponential backoff is reasonable. More than that can cause long delays (see the calculator) and may violate the API’s rate limits. Always check the API’s terms.

What if the server returns a 200 with an error in the body?

This is a common anti-pattern. Your client should check the response body for an error flag or code, even on 200. If the body indicates failure, treat it as a server error and retry if appropriate, but be careful not to retry idempotent operations that may have already succeeded.

Related

Formula used
in t | Timeout (seconds) | s | 10 | 1 | 300
in r | Max retries | - | 3 | 0 | 10
in b | Base backoff (ms) | ms | 500 | 100 | 10000
out total | Worst-case wait (seconds) | s | (b/1000) * (2^r - 1) | 2

Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →