Cloud Run: which client IP can I safely use for rate limiting?

Hi everyone,

I’m building a small Node.js service on Cloud Run and want to limit how many
requests one client IP can make. I’m trying to understand which address I can
trust, since callers can supply their own X-Forwarded-For header.

Requests go directly to Cloud Run’s default run.app URLs. I haven’t added a
load balancer, Firebase Hosting rewrite, custom domain or reverse proxy. The
test service uses gen1 in us-central1, with HTTP/1.1 to the container.

I tested both default URL forms: the one containing the project number and the
hash-based a.run.app URL. Both gave the same pattern:

Header I sent: (none)
Header received: OBSERVED_IP

Header I sent: 192.0.2.1
Header received: 192.0.2.1, OBSERVED_IP

Header I sent: 192.0.2.1, 198.51.100.2
Header received: 192.0.2.1, 198.51.100.2, OBSERVED_IP

The example addresses are deliberately fake. OBSERVED_IP replaces the actual
address in the results; I haven’t independently confirmed it is my outgoing
public IP. The socket’s remote address was an internal address.

**Is the last address in this header added by Cloud Run in a way the caller
cannot forge, and is it safe to use that address for IP-based rate limiting?
**
My test service currently requires IAM authentication. The eventual mobile
endpoint would allow requests through Cloud Run IAM and validate Firebase App
Check in the application. Does that change the answer? Are there differences
between the two default URLs, or with IPv6 or HTTP/2 clients?

I’ve read the Cloud Run container contract
and the load-balancer header documentation,
but I’m unsure which rule applies to direct run.app requests.

I’d appreciate a documentation link or clarification of the supported behavior.
If using the last address isn’t reliable, what would you recommend for this
setup?

Thanks for your help!

I found a closely related issue in Google’s Cloud Run sample repository: issue #3583.

The merged fix, PR #3586, sets app.set('trust proxy', 1) specifically for client-IP rate limiting behind Cloud Run.

This agrees with what I observed in my tests. Is that still the supported approach for direct run.app requests, including when a caller supplies their own X-Forwarded-For header?

Thanks—this looks promising, and I’d appreciate confirmation that it applies to the setup described above.