Blog

Cloud SQL Proxy: connection refused on 127.0.0.1:5432

A connection refused on 127.0.0.1 says nothing about the network. The Cloud SQL Proxy is not listening where your application is calling: either it is not running, or it bound a different port, or it bound a different address. One command, lsof -nP -iTCP:5432 -sTCP:LISTEN, separates the three, and empty output is already an answer.

Is the proxy actually running?

Ask this before anything else, and answer it without starting a thing. lsof lists listening sockets and names the process holding each one. When it prints nothing, nothing is listening on that port, and the rest of this page does not apply to you: start the proxy.

$ lsof -nP -iTCP:5432 -sTCP:LISTEN
$ echo $?
1

No output, exit status 1. The port is closed, and every client says so in its own dialect. The three messages below were produced in the same minute, on the same machine, against that same closed port.

$ go run dial_en.go 127.0.0.1:5432
dial tcp 127.0.0.1:5432: connect: connection refused
$ node -e 'require("net").connect(5432,"127.0.0.1").on("error",e=>console.log(e.message))'
connect ECONNREFUSED 127.0.0.1:5432
$ psql "host=127.0.0.1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused
	Is the server running on that host and accepting TCP/IP connections?
ClientWhat it printsWhat it means
A Go programdial tcp 127.0.0.1:5432: connect: connection refusedThe kernel answered immediately: no process is listening
Node.jsconnect ECONNREFUSED 127.0.0.1:5432The same refusal under a different name
psqlfailed: Connection refusedThe same refusal, with the right question on the next line

None of the three is a Cloud SQL message. They are all produced locally, before a single byte reaches Google, which is why searching the error text for a Cloud SQL cause turns up nothing useful.

The port and the address are in the same line

Once the proxy is up, lsof names it, and the tail of the line carries the two settings that go wrong most often. Below, it is listening, but on 192.168.1.99 rather than on loopback.

$ lsof -nP -iTCP:5432 -sTCP:LISTEN
COMMAND       PID    USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
cloud-sql 1463977 leandre 6u  IPv4 7955141      0t0  TCP 192.168.1.99:5432 (LISTEN)
$ go run dial_en.go 127.0.0.1:5432
dial tcp 127.0.0.1:5432: connect: connection refused
$ go run dial_en.go 192.168.1.99:5432
connected

lsof truncates the command name to nine characters, cloud-sql, so grepping that column for cloud-sql-proxy will never match. The port sits in the same field, and a proxy started with --port 5434 while the application dials 5432 produces exactly the same refusal.

$ ss -ltnp '( sport = :5432 or sport = :5434 )'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096       127.0.0.1:5434      0.0.0.0:*    users:(("cloud-sql-proxy",pid=1455994,fd=6))

The proxy keeps 127.0.0.1 by default, which is the right default on a workstation: nobody else on the network can reach your database. In a container that same default breaks everything, because one container's loopback is not its neighbour's. The Cloud SQL Auth Proxy documentation, read on 19 August 2026, documents the --address flag for that case. Setting --address 0.0.0.0 then exposes the port to anything that can reach the container, which is a decision, not a copy and paste.

A proxy that died at startup leaves the same message

The worst version of this is a proxy that started, failed, and vanished while you were looking elsewhere. A local port that is already taken is enough: the proxy exits with status 1 and leaves three lines nobody reads once they have scrolled past in a container log.

$ cloud-sql-proxy --port 5433 my-project:europe-west1:demo-instance
2026/08/19 05:13:34 Authorizing with OAuth2 token
2026/08/19 05:13:34 [my-project:europe-west1:demo-instance] could not listen to address 127.0.0.1:5433: listen tcp4 127.0.0.1:5433: bind: address already in use
2026/08/19 05:13:34 Error starting proxy: [my-project:europe-west1:demo-instance] Unable to mount socket: listen tcp4 127.0.0.1:5433: bind: address already in use
2026/08/19 05:13:34 The proxy has encountered a terminal error: unable to start: [my-project:europe-west1:demo-instance] Unable to mount socket: listen tcp4 127.0.0.1:5433: bind: address already in use
$ echo $?
1

Unix socket mode adds a second way to die, and it is the surprising one. TCP mode opens its port without asking Google anything, but --unix-socket resolves the instance before mounting the socket and stops dead when that call fails. The directory stays empty, and the client stops talking about refusals and starts talking about a missing file.

$ cloud-sql-proxy --unix-socket /tmp/csql my-project:europe-west1:demo-instance
2026/08/19 05:17:55 Error starting proxy: [my-project:europe-west1:demo-instance] Unable to mount socket: 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., instanceDoesNotExist
$ ls /tmp/csql/
$ psql "host=/tmp/csql/my-project:europe-west1:demo-instance dbname=postgres user=postgres" -c 'select 1'
psql: error: connection to server on socket "/tmp/csql/my-project:europe-west1:demo-instance/.s.PGSQL.5432" failed: No such file or directory
	Is the server running locally and accepting connections on that socket?

One flag restores that honesty in TCP mode. --run-connection-test tells the proxy to attempt the connection right away and exit if it fails, so the failure belongs to the proxy at startup instead of showing up as an inexplicable refusal inside your application ten seconds later.

 cloud-sql-proxy \
   --port 5432 \
+  --run-connection-test \
   my-project:europe-west1:demo-instance
$ cloud-sql-proxy --port 5432 --run-connection-test my-project:europe-west1:demo-instance
2026/08/19 05:13:34 [my-project:europe-west1:demo-instance] Listening on 127.0.0.1:5432
2026/08/19 05:13:34 The proxy has started successfully and is ready for new connections!
2026/08/19 05:13:34 Connection test started
2026/08/19 05:13:34 Connection test failed
2026/08/19 05:13:34 The proxy has encountered a terminal error: 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., instanceDoesNotExist
$ echo $?
1

A typo in project:region:instance cannot cause this

This is the hour most often lost on this message, and it is lost for nothing. A wrong instance name does not stop the proxy from opening its port. The name below has two parts instead of three, it cannot designate any instance anywhere, and the proxy still reports success.

$ cloud-sql-proxy --port 5432 my-project:demo-instance
2026/08/19 05:13:18 Authorizing with OAuth2 token
2026/08/19 05:13:18 [my-project:demo-instance] Listening on 127.0.0.1:5432
2026/08/19 05:13:18 The proxy has started successfully and is ready for new connections!

A one part name, demo-instance, behaves identically. The shape of the connection name is only tested on the first real API call, which happens when a client first connects. While the port is closed, rereading your connection string cannot tell you anything.

--private-ip: what forgetting it actually breaks

The same reasoning applies to this flag, for the same reason. With it or without it, the proxy binds the port, announces that it is ready, and waits. Forgetting it costs you later, on a connection that opens and then dies.

$ cloud-sql-proxy --port 5432 --private-ip my-project:europe-west1:demo-instance
2026/08/19 05:17:49 [my-project:europe-west1:demo-instance] Listening on 127.0.0.1:5432
2026/08/19 05:17:49 The proxy has started successfully and is ready for new connections!
$ psql "host=127.0.0.1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql: error: connection to server at "127.0.0.1", port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.

Be precise about what that capture proves. Our test instance does not exist, so the refresh fails for that reason rather than over a private address, and the message shape is the one every failed refresh produces once the port is open. What it does establish is narrow and sufficient: --private-ip decides nothing before the first connection, and cannot close a port.

The messages, and what each one means

What the application seesState of the local portThe cause
connection refusedclosedThe proxy is down, or on another port, or on another address
server closed the connection unexpectedlyopenThe proxy is up and its Cloud SQL Admin API call failed
timeout expiredneither open nor closedPackets are dropped in transit: wrong address, or a firewall
No such file or directory on .s.PGSQL.5432not applicableUnix socket mode: the socket was never mounted

Row three is the one most readily mistaken for row one, although the two feel nothing alike in practice: a closed port answers instantly, while a silent address makes you wait out the timeout.

$ go run dial_en.go 10.255.255.1:5432
dial tcp 10.255.255.1:5432: i/o timeout
$ psql "host=10.255.255.1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql: error: connection to server at "10.255.255.1", port 5432 failed: timeout expired

Kestro only reports a tunnel as open after checking that the port answers, which is the check in the first section of this page. The guide to a proxy that refuses collects the other refusals in this family, the comparison with the Cloud SQL proxy sets out what each of the two actually does, and the 403 from a disabled API is what you read when the port is open after all.

What is left

These captures come from a Linux machine on 19 August 2026, running proxy 2.25.2+linux.amd64 and psql 16.15. lsof is the same tool on macOS and the command does not change by a character, but we did not replay it there today, and the COMMAND column may truncate differently.

The instance targeted by every test here does not exist. What we measured is therefore the proxy's local behaviour: what it binds, when it dies, what it prints. Nothing past the handshake with a real database was observed, and a refusal arriving at that stage would have a different story behind it.

The container case is argued from the mechanism rather than from a full run: we verified that listening on an address other than loopback makes 127.0.0.1 refuse, which is the same rule, but not the behaviour of a proxy in a container facing a real neighbour.

Finally, a port test only reports what was true in the second it ran. A proxy killed between your lsof and your connection will hand you a refusal that the previous reading contradicts, and that is the one case where the command in the first section misleads you without being wrong.