Library Name
@google-cloud/bigquery
A screenshot that you have tested with "Try this API".
The screenshots show the issue is client-side, not API-side:
jobs.query accepts jobTimeoutMs and returns results normally (HTTP 200).
jobs.query enforces it: a ~30s query with jobTimeoutMs: "1000" returns HTTP 499 with "Job execution was cancelled: Job timed out after 1 sec".
What would you like to see in the library?
This is a request for the handwritten client logic in handwritten/bigquery, not for the API surface: the API already supports the field, and the limitation is a client-side denylist.
Current behavior
BigQuery.query() routes queries to the accelerated jobs.query endpoint unless an option appears on a denylist in buildQueryRequest_().
jobTimeoutMs is on that list, so setting it silently forces every query through the slower jobs.insert + getQueryResults path:
|
!!queryObj.jobTimeoutMs || |
// always goes through jobs.insert + polling, even for a tiny query
const [rows] = await bigquery.query({query: 'SELECT 1', jobTimeoutMs: 60000});
The API now supports jobTimeoutMs on jobs.query
- The
jobs.query request body now has a jobTimeoutMs field. This repository's generated IQueryRequest type already includes it:
|
/** |
|
* Optional. Job timeout in milliseconds. If this time limit is exceeded, BigQuery will attempt to stop a longer job, but may not always succeed in canceling it before the job completes. For example, a job that takes more than 60 seconds to complete has a better chance of being stopped than a job that takes 10 seconds to complete. This timeout applies to the query even if a job does not need to be created. |
|
*/ |
|
jobTimeoutMs?: string; |
- The field's documentation explicitly covers the no-job case: "This timeout applies to the query even if a job does not need to be created."
Proposed change
Remove jobTimeoutMs from the denylist and map it into the IQueryRequest, converting the number to a string as the API expects an int64 field. This mirrors what createQueryJob() already does for the jobs.insert path.
A patch with a unit test is ready on my fork, based on current main. I will open a PR if the approach looks good: main...takaebato:google-cloud-node:bigquery-jobtimeoutms-fast-path
Describe alternatives you've considered
No real alternative: users today can only drop jobTimeoutMs or fall back to createQueryJob() and manage the job manually.
Additional context/notes
I verified the server-side behavior with direct REST calls.
1. jobTimeoutMs is enforced on the fast path, with the identical error as the slow path
A query that runs for roughly 30 seconds, submitted with a 1 second timeout via POST /projects/$PROJECT/queries:
{
"query": "SELECT COUNT(*) FROM UNNEST(GENERATE_ARRAY(1, 1000000)) a CROSS JOIN UNNEST(GENERATE_ARRAY(1, 1000)) b WHERE MOD(a + b, 7) = 0",
"useLegacySql": false,
"useQueryCache": false,
"jobTimeoutMs": "1000",
"timeoutMs": 120000
}
returns HTTP 499 synchronously after about 7 seconds:
{
"error": {
"code": 499,
"message": "Job execution was cancelled: Job timed out after 1 sec",
"errors": [
{
"message": "Job execution was cancelled: Job timed out after 1 sec",
"domain": "global",
"reason": "stopped"
}
],
"status": "CANCELLED"
}
}
The same query submitted via POST /projects/$PROJECT/jobs with {"configuration": {"query": {...}, "jobTimeoutMs": "1000"}} ends as DONE with an errorResult carrying the identical reason and message, and jobs.getQueryResults for that job returns a byte-identical HTTP 499 body. So the error surfaced to the client is the same regardless of path.
2. The timeout applies even when no job is created
The same fast path request with "jobCreationMode": "JOB_CREATION_OPTIONAL" added was cancelled as well, with the same error shape, confirming the documented behavior ("This timeout applies to the query even if a job does not need to be created").
3. The jobTimeoutMs: 0 case: only the request body changes, not the behavior
Before the patch, query({query, jobTimeoutMs: 0}) already took the fast path, because the denylist check is truthiness-based and 0 is falsy. The field was not sent in that case. With the patch, the field is sent as "jobTimeoutMs": "0". The server accepts "0" and treats it as "no timeout", so the observable behavior is the same, verified with
{"query": "SELECT 1", "useLegacySql": false, "jobTimeoutMs": "0"}
returning HTTP 200 with results. This also matches createQueryJob(), which has always sent "0" on the jobs.insert path.
Library Name
@google-cloud/bigquery
A screenshot that you have tested with "Try this API".
The screenshots show the issue is client-side, not API-side:
jobs.queryacceptsjobTimeoutMsand returns results normally (HTTP 200).jobs.queryenforces it: a ~30s query withjobTimeoutMs: "1000"returns HTTP 499 with"Job execution was cancelled: Job timed out after 1 sec".What would you like to see in the library?
This is a request for the handwritten client logic in
handwritten/bigquery, not for the API surface: the API already supports the field, and the limitation is a client-side denylist.Current behavior
BigQuery.query()routes queries to the acceleratedjobs.queryendpoint unless an option appears on a denylist inbuildQueryRequest_().jobTimeoutMsis on that list, so setting it silently forces every query through the slowerjobs.insert+getQueryResultspath:google-cloud-node/handwritten/bigquery/src/bigquery.ts
Line 2354 in 13d1ca8
The API now supports
jobTimeoutMsonjobs.queryjobs.queryrequest body now has ajobTimeoutMsfield. This repository's generatedIQueryRequesttype already includes it:google-cloud-node/handwritten/bigquery/src/types.d.ts
Lines 4437 to 4440 in 13d1ca8
Proposed change
Remove
jobTimeoutMsfrom the denylist and map it into theIQueryRequest, converting the number to a string as the API expects an int64 field. This mirrors whatcreateQueryJob()already does for thejobs.insertpath.A patch with a unit test is ready on my fork, based on current main. I will open a PR if the approach looks good: main...takaebato:google-cloud-node:bigquery-jobtimeoutms-fast-path
Describe alternatives you've considered
No real alternative: users today can only drop
jobTimeoutMsor fall back tocreateQueryJob()and manage the job manually.Additional context/notes
I verified the server-side behavior with direct REST calls.
1.
jobTimeoutMsis enforced on the fast path, with the identical error as the slow pathA query that runs for roughly 30 seconds, submitted with a 1 second timeout via
POST /projects/$PROJECT/queries:{ "query": "SELECT COUNT(*) FROM UNNEST(GENERATE_ARRAY(1, 1000000)) a CROSS JOIN UNNEST(GENERATE_ARRAY(1, 1000)) b WHERE MOD(a + b, 7) = 0", "useLegacySql": false, "useQueryCache": false, "jobTimeoutMs": "1000", "timeoutMs": 120000 }returns HTTP 499 synchronously after about 7 seconds:
{ "error": { "code": 499, "message": "Job execution was cancelled: Job timed out after 1 sec", "errors": [ { "message": "Job execution was cancelled: Job timed out after 1 sec", "domain": "global", "reason": "stopped" } ], "status": "CANCELLED" } }The same query submitted via
POST /projects/$PROJECT/jobswith{"configuration": {"query": {...}, "jobTimeoutMs": "1000"}}ends as DONE with anerrorResultcarrying the identical reason and message, andjobs.getQueryResultsfor that job returns a byte-identical HTTP 499 body. So the error surfaced to the client is the same regardless of path.2. The timeout applies even when no job is created
The same fast path request with
"jobCreationMode": "JOB_CREATION_OPTIONAL"added was cancelled as well, with the same error shape, confirming the documented behavior ("This timeout applies to the query even if a job does not need to be created").3. The
jobTimeoutMs: 0case: only the request body changes, not the behaviorBefore the patch,
query({query, jobTimeoutMs: 0})already took the fast path, because the denylist check is truthiness-based and 0 is falsy. The field was not sent in that case. With the patch, the field is sent as"jobTimeoutMs": "0". The server accepts"0"and treats it as "no timeout", so the observable behavior is the same, verified with{"query": "SELECT 1", "useLegacySql": false, "jobTimeoutMs": "0"}returning HTTP 200 with results. This also matches
createQueryJob(), which has always sent"0"on thejobs.insertpath.