Cloud SQL Proxy 403: Admin API has not been used in project
Published August 18, 2026
The Cloud SQL Proxy fails with Error 403: Cloud SQL Admin API has not been used in project when that API is disabled on the project Google bills for the call. The message names that project three times, and it is not always the one holding your instance. A missing roles/cloudsql.client raises a different 403, with a different reason code, so the two are worth telling apart before you change anything.
What the message actually names
Here is the real session, with proxy v2.25.2 against a project where the Cloud SQL Admin API had never been called. The instance does not exist, which turns out not to matter: the 403 lands before that question is ever asked.
$ cloud-sql-proxy --quota-project my-project --port 5432 my-project:europe-west1:demo-instance
2026/08/18 20:18:25 Authorizing with OAuth2 token
2026/08/18 20:18:25 [my-project:europe-west1:demo-instance] Listening on 127.0.0.1:5432
2026/08/18 20:18:30 The proxy has started successfully and is ready for new connections!
2026/08/18 20:18:34 [my-project:europe-west1:demo-instance] failed to connect to instance: failed to get instance: refresh error: failed to get instance metadata (connection name = "my-project:europe-west1:demo-instance"): googleapi: Error 403: Cloud SQL Admin API has not been used in project my-project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/sqladmin.googleapis.com/overview?project=my-project then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.Three things are buried in that line, and they are the only ones that matter. The project name is the consumer of the API call, the one Google charges quota to, which is a different concept from the project that owns the instance. The activation URL is complete and works as pasted. The closing sentence promises a propagation delay, and it is the reason people give up one retry too early.
The response body also carries a machine readable reason, SERVICE_DISABLED, under details. Match on that in a log, never on the English prose, which Google is free to reword.
Why did the proxy say it started successfully?
Because it never calls the Cloud SQL Admin API at startup. It binds the local port, prints The proxy has started successfully and is ready for new connections!, and waits. The first API call goes out when a client actually shows up on that port, and only then does the 403 appear.
That gap is where the wasted hours come from. A startup script that waits for ready for new connections sees the line, decides the dependency is healthy, and starts the application; the failure then surfaces on the application side as a refused or dropped connection that no longer looks anything like an API problem.
Turning the API on, and how long that really takes
Enabling it is one command, and the error message hands you the console link if you would rather click.
gcloud services enable sqladmin.googleapis.com --project=my-project
The open question is the one the documentation never answers: how long before the proxy works? The message says "a few minutes". We measured it twice on 18 August 2026, on a project where the API had never been enabled, polling every two seconds from the moment the command was launched.
$ ./measure.sh
before enabling: HTTP 403
T0 = 18:11:11 : gcloud services enable
Operation "operations/acat.p2-...-104773e9cada" finished successfully.
command returned after 6 s
t+6s : HTTP 403
t+8s : HTTP 404
FIRST NON-403 AT t+8s8 seconds on the first run, 6 seconds on the second. The flip from 403 to 404 is the signal: 404 means the API is answering and is now looking for an instance called demo-instance, which genuinely does not exist. The door is open.
Two details are worth keeping rather than rounding away. gcloud services enable returns before the change is effective: on the first run, a call issued immediately after it returned still got a 403. And two measurements on an empty project on one day are not a service guarantee. Google says "a few minutes" and owes you nothing faster.
The second 403: a role, not an API
The other 403 says nothing about APIs and is almost too terse to act on. A service account with no roles at all, on a project where the API is properly enabled, gets this:
$ curl -s -H "Authorization: Bearer $TOKEN" -H "x-goog-user-project: my-project" \
"https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project/instances/demo-instance"
{
"error": {
"code": 403,
"message": "The client is not authorized to make this request.",
"errors": [
{
"message": "The client is not authorized to make this request.",
"domain": "global",
"reason": "notAuthorized"
}
]
}
}notAuthorized replaces SERVICE_DISABLED, and that reason code is the only dependable difference between the two situations. One role flips it: after granting roles/cloudsql.client to the same account, the identical request answered 404, meaning authorized but no such instance.
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:[email protected]" \
--role="roles/cloudsql.client"
roles/editor also clears the error, and that is exactly what is wrong with it: it grants write access across nearly every resource in the project in order to obtain one permission, cloudsql.instances.connect. The Cloud SQL roles documentation, read on 18 August 2026, describes roles/cloudsql.client as the role meant for connecting, and a developer workstation has no reason to hold more.
The 404 that is really this 403
Without a quota project attached to your credentials, the same project with the same disabled API stops returning 403 and returns 404 instead:
$ cloud-sql-proxy --port 5432 my-project:europe-west1:demo-instance
2026/08/18 20:18:38 The proxy has started successfully and is ready for new connections!
2026/08/18 20:18:43 [my-project:europe-west1:demo-instance] failed to connect to instance: failed to get instance: refresh error: failed to get instance metadata (connection name = "my-project:europe-west1:demo-instance"): googleapi: Error 404: The Cloud SQL instance does not exist., instanceDoesNotExistSame project, same disabled API, same minute. Only the --quota-project flag is gone. The message now sends you hunting for a typo in a connection name that is perfectly correct. The quota project documentation, read on 18 August 2026, sets out which project Google bills depending on the kind of credentials in play. The practical takeaway is shorter: a 404 from the proxy is not proof that the instance is missing.
| What you see | Reason code | What is actually wrong |
|---|---|---|
Cloud SQL Admin API has not been used in project ... | SERVICE_DISABLED | The API is off on the consumer project |
The client is not authorized to make this request. | notAuthorized | The account lacks roles/cloudsql.client |
The Cloud SQL instance does not exist. | instanceDoesNotExist | Wrong project:region:instance, or the API is off and no quota project is attached |
could not find default credentials | none | No ADC and no --token: the proxy exits before any API call |
Kestro composes the official proxy command and surfaces this output verbatim, reason code included, instead of a silent red dot. The guide to a proxy that refuses collects the other refusals of the same family, and the comparison with the Cloud SQL proxy says what each of the two actually does.
What is left
Our two propagation numbers describe a fresh project with no special quota, on one Tuesday in August. An organisation with policy constraints, a project under constraints/serviceuser.services, or an activation performed by an account other than yours, can all stretch that delay with nothing on screen to say so.
The nastiest variant is still an API enabled on the instance's project but disabled on the quota project. The message names one project, you check the other, and both look correct. Read the name printed in the message, not the one in your connection string.
We also did not measure what happens to an account whose role was just revoked. Removal took more than a minute to become visible here, and an already issued token keeps working for a while: if you are cutting off access for real, do not count on the next second.