Retries, timeouts and rate limits
What the transport retries and what it never retries, 401 recovery, the 429 path, the circuit breaker, and the fixed timeouts.
Default retry policy#
RetryPolicy.defaultPolicy() applies to every client unless EquinixConfig supplies another.
| Property | Default |
|---|---|
| Retries after the first attempt | 3 |
| Retryable statuses | 429, 500, 502, 503, 504 |
Transport IOException | Retried |
| Backoff | Exponential with full jitter, base 500 ms, cap 20 s |
Retry-After | Honoured, as delta-seconds or an HTTP-date |
Idempotency rule#
A 5xx response or an IOException does not say whether the server processed the request, and a create that is sent again after the server processed it produces a second resource. The SDK therefore retries those failures only for idempotent methods, which it defines as every method except POST and PATCH. PATCH is excluded because the SDK sends RFC 6902 documents, and an add operation applied twice changes the result.
A 429 response states that the server did not process the request, so the SDK retries a 429 for every method, as long as 429 is in the retryable set.
| Failure | GET, PUT, DELETE | POST, PATCH |
|---|---|---|
| 429 | Retried | Retried |
| 500, 502, 503, 504 | Retried | Not retried |
IOException | Retried | Not retried |
| Any other 4xx | Not retried | Not retried |
Under this rule the default policy does not retry a POST .../search on a 5xx, although a search has no side effect. A client that sends only reads can enable retries for POST and PATCH.
import com.eqixiac.equinix.EquinixConfig;
import com.eqixiac.equinix.Fabric;
import com.eqixiac.equinix.core.http.RetryPolicy;
RetryPolicy readOnlyPolicy = RetryPolicy.builder()
.maxRetries(5)
.baseDelayMillis(250)
.maxDelayMillis(10_000)
.retryableStatusCodes(Set.of(429, 500, 502, 503, 504))
.retryOnIoException(true)
.honorRetryAfter(true)
.retryNonIdempotentMethods(true) // only for a client that sends no creates or patches
.build();
Fabric reader = new Fabric(credentials, EquinixConfig.builder().retryPolicy(readOnlyPolicy).build());
Fabric noRetries = new Fabric(credentials, EquinixConfig.builder().retryPolicy(RetryPolicy.none()).build());Do not set retryNonIdempotentMethods(true) on a client that creates resources. The Equinix APIs covered here define no idempotency key.
401 recovery New in 4.0#
When an authenticated request is answered with HTTP 401, the client discards the cached token, obtains a new one and replays the request once. If the replayed request is also answered with 401, the client throws EquinixAuthenticationException. When several concurrent requests are rejected with the same token, the client sends one token request for all of them.
The replay is safe for every method, because a 401 means the request was rejected before it was processed. There is no replay for the token request itself, for the operations declared unauthenticated (the STS exchange and discovery), or when the client has no token source.
With a BearerTokenProvider the recovery calls the provider again. A provider built with BearerTokenProvider.of(token) returns the same token, so the second attempt also fails and the exception propagates.
Rate limits#
The SDK has no client-side request throttle. It handles an HTTP 429 response as follows.
- A 429 is retried up to the policy's limit, waiting for the
Retry-Aftervalue when the response carries one and for the computed backoff otherwise. - When the retries are exhausted the client throws
EquinixRateLimitException. ItsgetHttpHeaders()map carries the response headers, includingRetry-After. - A 429 does not count as a failure for the circuit breaker.
A batch job that fans out over many resources should bound its own concurrency. The connection pool allows 20 connections per route and 50 in total, which is the upper bound on parallel requests per session.
PeeringDB, which Peering Intelligence queries, has its own limit. Anonymous access allows about 20 requests per minute. On 2026-11-11 PeeringDB throttled anonymous calls from the test account for about an hour.
Circuit breaker#
The circuit breaker is disabled by default. When enabled, one breaker counts consecutive failures across all requests of a client.
import com.eqixiac.equinix.Equinix;
import com.eqixiac.equinix.core.exception.CircuitOpenException;
import com.eqixiac.equinix.core.http.CircuitBreaker;
EquinixConfig config = EquinixConfig.builder()
.circuitBreaker(new CircuitBreaker(5, 30_000)) // threshold, cooldown in ms
.build();
try (Equinix eq = new Equinix(credentials, config)) {
eq.fabric().ports().list();
} catch (CircuitOpenException e) {
// no HTTP exchange took place; the breaker is open
}| State | Behaviour |
|---|---|
CLOSED | Requests pass. A 5xx response or an IOException increments the counter. Any completed exchange with a status below 500 resets it. At the threshold the breaker opens. |
OPEN | Every request fails at once with CircuitOpenException until the cooldown elapses. Results of requests admitted before the breaker opened are ignored. |
HALF_OPEN | One probe request is admitted. Its success closes the breaker and its failure opens it for another cooldown. A probe slot expires after a further cooldown, so a probe that never reports cannot block the breaker. |
Each retry attempt also passes through the breaker. A 4xx response is not counted as a failure, because the service answered the request.
Timeouts#
| Timeout | Value |
|---|---|
| Connect | 10 s |
| Socket read | 60 s |
| Lease a pooled connection | 5 s |
These values are constants of the HTTP client, and EquinixConfig has no option for them in the current tree. The worst case for one idempotent call under the default policy is four attempts of up to 60 s each plus up to three backoff waits of at most 20 s each.
The live rate-card adapters in the design layer use their own client with 5 s connect and 10 s socket timeouts, see Cost and value.
Correlation identifiers#
The client assigns one X-Correlation-Id header value to each logical request and sends the same value on every retry attempt. The value appears in the debug log and on EquinixServiceException.getCorrelationId(), so a failed call can be matched to its log lines and quoted in a support case.