---
cuppy_hub: gcp
cuppy_path: guides/data/query-bigquery-from-typescript.md
cuppy_section: docs
cuppy_doc: DCMH-3D1K
cuppy_doc_uuid: e1a10c31-33ea-4741-aeb7-8aba7f83de4a
cuppy_permalink: https://cuppy.io/gcp/d/DCMH-3D1K
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/gcp/docs/guides/data/query-bigquery-from-typescript
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: cd1fbafc012771a3d61c95eb75821425ff2bd33e8d2495754a91fc66a026b079
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Query BigQuery from TypeScript

## Step 1 — the official client

```
npm i @google-cloud/bigquery
```

**Check the installed package's own type definitions for every option you use.** Google Cloud client libraries
rename and move options between major versions, and the `.d.ts` in `node_modules` is the only authority for the
version you actually have. This hub does not reproduce the API surface for that reason.

## Step 2 — no key, ever

Construct the client with **no credentials argument**. It picks up the ambient identity: the attached service
account on Cloud Run, Compute Engine, GKE with Workload Identity Federation, or your impersonated credentials
locally.

Service account keys are "a security risk if not managed correctly", and the attached-identity path exists so
that none is needed (`reference/identity/predefined-roles-and-permissions.md` §5).

## Step 3 — parameterise the query

Pass values as **query parameters**, not as interpolated strings. Two reasons, and the second is the one people
forget:

1. string interpolation into SQL is an injection vector;
2. a parameterised query is the **same query text** every time, which matters for caching and for reading your
   own slot usage.

## Step 4 — handle exactly the errors the vendor says to retry

| Retry, with exponential backoff | Never retry |
|---|---|
| `backendError` | `invalidQuery` — the SQL is wrong |
| `internalError` | `billingTierLimitExceeded` — the **on-demand model** refused a query of that size. Retrying changes nothing; reduce the scan, or move to capacity pricing |
| `jobBackendError` | `accessDenied` — a permission, not a transient |
| `jobInternalError` | `resourcesExceeded` — the query has a stage that will not distribute |
| `rateLimitExceeded` | |

A client that retries the right-hand column is the most common wasteful pattern in a BigQuery integration: it
turns a clear failure into a slow one, and on an on-demand project it can turn a refusal into a bill.

## Step 5 — the two grants, which are separate

| To | Role |
|---|---|
| run a query (create a job) | a job-running role such as `roles/bigquery.jobUser` |
| read the data it touches | a data-reading role such as `roles/bigquery.dataViewer`, on the **dataset** |

**Job permission and data permission are different grants on different objects.** An identity with only the
first gets `accessDenied` on the data; with only the second it cannot start the job. Verify the exact role
names against the current roles reference before writing them into IaC
(`reference/identity/predefined-roles-and-permissions.md`).

## Step 6 — verify

| Check | Expected |
|---|---|
| run the query from the deployed workload | rows, with **no key** in the deployment |
| run it with a deliberately wrong column name | `invalidQuery` — and your client does **not** retry |
| remove the dataset-level read role | `accessDenied` — proving which grant carries the data permission |

## For a migration

This is also the smallest honest benchmark for a warehouse migration: the same query, parameterised, run
against the source and against BigQuery, with **bytes scanned** recorded. That number — not a node count — is
the input a BigQuery cost estimate needs (`guides/parity/data.md` §2).


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 91 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/gcp/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
