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.
- First retry delay: 500 ms
- Second retry delay: 1000 ms
- Third retry delay: 2000 ms
- 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-Afterheader. - Use exponential backoff: wait
b, then2b, then4b, etc. - Add jitter: randomize the delay to prevent synchronized retry storms.
- Respect the server’s
Retry-Afterheader 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:
- Send initial request with no cursor.
- Extract
next_cursorfrom the JSON response. - If the cursor is null or empty, stop. Otherwise, send the next request with that cursor.
- 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-Afterheader, 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:
- Create an
HttpClientwith a connect timeout and a reasonable read timeout. - Write a method that sends a request and returns the response body as a string.
- In that method, loop up to
maxRetriestimes. - On each catch of
IOExceptionorInterruptedException, or a 5xx/429 status, compute backoff delayb * 2^attemptand sleep (with jitter). - On 4xx (except 429), break and return the error.
- 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 useBodyHandlers.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
- Valheim Server Not Showing in Community List Fixes
- DisplayPort vs HDMI: Which Cable Should You Use?
- Soundbar vs 2.1 Speaker System: Which Is Better?
- Survive Lava for Brainrot Tier List170 points
- MySQL Dump Import: Fix Common Errors
- Mouse Moving on Its Own? Causes & Fixes