Repository navigation
Default retry logic does not retry HTTP 503 errors from BigQuery API on BigQuery.getTable() #12885
Description
[bigquery]: Default retry logic does not retry HTTP 503 errors from BigQuery API on BigQuery.getTable()
When a BigQuery RPC such as BigQuery.getTable(...) receives a 503 backendError response, the call fails immediately on the first attempt. The configured RetrySettings (maxAttempts, backoff, etc.) are not honored, even though 503 is declared as retryable by the SDK itself in BigQueryException.RETRYABLE_ERRORS.
Environment details
- API: BigQuery
- Java version: 21
- Version(s):
com.google.cloud:google-cloud-bigquery:2.62.0com.google.http-client:google-http-client-test:1.44.1(used only by the reproduction below to mock the transport)
Steps to reproduce
- Build and run the code under "Code example" with
com.google.cloud:google-cloud-bigquery:2.62.0. - The mock transport returns HTTP 503 on the first request and HTTP 200 on the second. With
RetrySettings.setMaxAttempts(5)configured, two requests are expected (first is retried, second succeeds). - Observe that only one request is sent and a
BigQueryExceptionis thrown immediately - the retry never happens.
Code example
package com.example;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.bigquery.BigQuery;
import com.google.cloud.bigquery.BigQueryOptions;
import com.google.cloud.bigquery.Table;
import com.google.cloud.bigquery.TableId;
import com.google.cloud.http.HttpTransportOptions;
import org.threeten.bp.Duration;
import java.util.Date;
public class Repro {
public static void main(String[] args) {
MockHttpTransport mockTransport = new MockHttpTransport() {
private int requestCount = 0;
@Override
public MockLowLevelHttpRequest buildRequest(String method, String url) {
return new MockLowLevelHttpRequest() {
@Override
public MockLowLevelHttpResponse execute() {
requestCount++;
System.out.println("[Mock] Intercepted request #" + requestCount + " to: " + url);
if (requestCount < 2) {
String jsonError = """
{
"error": {
"code": 503,
"message": "Visibility check was unavailable. Please retry the request",
"errors": [{"reason": "backendError", "domain": "global"}]
}
}
""";
return new MockLowLevelHttpResponse()
.setStatusCode(503)
.setContentType("application/json")
.setContent(jsonError);
}
String tableJson = """
{
"kind": "bigquery#table",
"id": "test-project:test_dataset.test_table",
"type": "TABLE",
"tableReference": {
"projectId": "test-project",
"datasetId": "test_dataset",
"tableId": "test_table"
}
}
""";
return new MockLowLevelHttpResponse()
.setStatusCode(200)
.setContentType("application/json")
.setContent(tableJson);
}
};
}
};
BigQueryOptions options = BigQueryOptions.newBuilder()
.setProjectId("test-project")
.setCredentials(GoogleCredentials.create(
new AccessToken("dummy-token", new Date(System.currentTimeMillis() + 3600000))))
.setRetrySettings(RetrySettings.newBuilder()
.setMaxAttempts(5)
.setInitialRetryDelay(Duration.ofSeconds(1))
.setRetryDelayMultiplier(2.0)
.setMaxRetryDelay(Duration.ofSeconds(10))
.setTotalTimeout(Duration.ofSeconds(30))
.build())
.setTransportOptions(HttpTransportOptions.newBuilder()
.setHttpTransportFactory(() -> mockTransport)
.build())
.build();
BigQuery bigquery = options.getService();
Table table = bigquery.getTable(TableId.of("test_dataset", "test_table"));
System.out.println("Got table: " + table.getTableId().getTable());
}
}Stack trace
[Mock] Intercepted request #1 to: https://bigquery.googleapis.com/...
Exception in thread "main" com.google.cloud.bigquery.BigQueryException: Visibility check was unavailable. Please retry the request
at com.google.cloud.bigquery.BigQueryRetryHelper.runWithRetries(...)
...
Only one request is made. Expected: two requests, then a successful Table.
External references such as API reference guides
com.google.cloud.bigquery.BigQueryException.RETRYABLE_ERRORSingoogleapis/java-bigquery- lists{500, 502, 503, 504}as retryable.BigQueryOptions.Builder.setResultRetryAlgorithm(...)- the public hook that currently has to be used to make 5xx retries actually happen.
Any additional information below
Expected behavior. A 503 backendError response is retried according to the configured RetrySettings, because:
BigQueryException.RETRYABLE_ERRORSexplicitly lists{500, 502, 503, 504}as retryable.- BigQuery's public documentation describes 5xx as transient and retryable.
- A user configuring
RetrySettings.setMaxAttempts(5)reasonably expects 5xx to be included.
Actual behavior. The request fails on the first attempt. Total elapsed time is a single RPC round-trip - no backoff, no retry.
Why this matters / how it was discovered. The issue surfaced during a production incidents: the BigQuery API (GET https://bigquery.googleapis.com/bigquery/v2/projects/<EXMAPLE_PROJECT>/datasets/<EXAMPLE_DATASET>>/tables/<EXAMPLE_TABLE>?prettyPrint=false&view=STORAGE_STATS) sporadically returned
{
"error": {
"code": 503,
"message": "Visibility check was unavailable. Please retry the request",
"errors": [{"reason": "backendError", "domain": "global"}]
}
}in response to bigquery.getTable(...) calls. The default retry policy in google-cloud-bigquery did not retry these errors, and there is no straightforward configuration-level way for users to enable retrying of 5xx responses without replacing the retry algorithm wholesale via setResultRetryAlgorithm(...).
Happy to provide any additional context if useful.
Activity
[-]Default retry logic does not retry HTTP 503 errors on `BigQuery.getTable()`[/-][+]Default retry logic does not retry HTTP 503 errors from BigQuery API on `BigQuery.getTable()`[/+]lqiu96 commentedon Apr 29, 2026
Can you link me to the public documentation about this? I see references to
https://cloud.google.com/bigquery/troubleshooting-errorsbut that leads me to a 404. From a quick search, I think the new guide would be here:https://docs.cloud.google.com/bigquery/docs/error-messageswhich I can update references.I agree that retrying on the 5xx errors would a better UX. As of right now, the public doc suggests that the behavior is WAI as the expectation is that users will need to configure retries. I'll talk to the internal team about any concerns with client-side retries enabled by default.
patrykks commentedon May 1, 2026
Thanks for the response and for taking a look at this I really appreciate the attention to the issue.
I’d like to clarify what I meant regarding the documentation, as I think we’re referring to two slightly different layers:
I fully agree here. If someone is using the API directly, then implementing retries is the client’s responsibility. I was referring to the same documentation you mentioned:
“5xx errors are service-side issues (…) you need to retry your requests using truncated exponential backoffs.”
This is clear and makes perfect sense at the API level.
My concern is specifically about how this is handled in the SDK layer, which already introduces its own retry abstraction.
In the Java client library:
Given that, there are a few points that make the current behavior surprising:
Additionally, from the library’s internal contract:
From a user perspective, this creates an inconsistency:
This effectively forces users to:
either reimplement retry logic on top of the SDK
or wrap calls defensively, which partially defeats the purpose of having a client library abstraction
I don’t dispute that this is “working as intended” at the API level. However, at the SDK level, this behavior appears inconsistent with:
For that reason, treating this as a bug still seems justified from the client library perspective.
Happy to hear more context if there are constraints or design decisions that explain why retries are not applied here
patrykks commentedon May 2, 2026
My previous explanation might not have been precise enough, so I’d like to add more context on why I expected the SDK to retry these errors.
Additional context from SDK behavior
When using the client like this:
the library internally calls:
This uses:
My understanding is that if the user does not explicitly configure a
resultRetryAlgorithm, the default is used.From
BigQueryOptions:So effectively, the default is:
DEFAULT_BIGQUERY_EXCEPTION_HANDLERwhich is defined as:
The key part here is the
EXCEPTION_HANDLER_INTERCEPTOR, which behaves as follows:Observed behavior
From the user perspective, the error returned is:
This corresponds to an HTTP 503, which:
BaseServiceException.isRetryable()) should also be considered retryable by the interceptorWhere the inconsistency seems to come from
The reason it is not retried appears to be the use of:
getTableSkipExceptionTranslationThe retry wrapper (
runWithRetries) operates on methods that do not yet translate exceptions intoBigQueryException.Only after the retry block completes, the exception is translated and surfaced to the user as
BigQueryException.As a result:
isRetryable) is effectively tied toBaseServiceException/BigQueryExceptionWhy this feels problematic from a user perspective
From the outside, this creates a mismatch:
This makes it difficult to reason about expected behavior and forces users to:
which somewhat undermines the value of having a client library abstraction.
It’s possible I’m missing some internal design constraint here, but from my perspective this does not fully align with “working as intended” at the SDK level.
I’d really appreciate any clarification on whether:
Thanks again for taking the time to look into this. Happy to dig deeper if needed.
lqiu96 commentedon May 4, 2026
I think your confusion is absolutely reasonable. From what I've been gathering, the retries configured in Bigquery are a bit confusing and not in an ideal state.
I'm still probing around and I think the changes that I'm leaning towards (this is also what I'm seeing in other languages) is that a lot of the query operations should retry on 5xx errors. I'm going to leave the stuff with jobs as-is.
I think we'll slowly make these changes to try and improve this behavior. I won't be able to promise a timeline on this, but we are absolutely happy to review any contributions and PRs
goutamadwant commentedon Jun 6, 2026
@lqiu96 i was able to reproduce this with a focused regression test around BigQuery.getTable()
The test throws the same BigQuery JSON HTTP 503 shape from getTableSkipExceptionTranslation.. on current main it does not retry because the retry helper sees HttpResponseException before it is translated to BigQueryException so the existing retry check for 503 is not reached
I opened a small PR for getTable. It translates HttpResponseException inside the retry callable so the existing BigQueryException retry rules can handle transient server errors. Kept it scoped to getTable and did not touch job retry behavior or raw network IOExceptions
I did not use a live BigQuery 503 for this since that is not something we can trigger deterministically. Pls review to see if this makes sense
lqiu96 commentedon Jun 26, 2026
Should be resolved via #13416