We recently migrated CodeCrafters from Heroku to Render. An hour later we had customers complaining that our PPP discounts weren’t showing. Turns out this was because Rails’s request.remote_ip behaves differently on Heroku vs. Render and returns a proxy’s IP instead of the client’s IP.
If you’re using Rails on Render, read on to understand why this happens and how to fix it.
How request.remote_ip works
When a request arrives at your Rails app, it might have passed through
multiple proxies. Each proxy typically appends its IP to the
X-Forwarded-For header, building a chain like this:
X-Forwarded-For: <client>, <proxy1>, <proxy2>
Rails’ request.remote_ip walks this chain right-to-left, skipping
any IPs it considers “trusted proxies” (private/reserved IP ranges by
default). The first non-trusted IP it encounters is what it returns as
the client’s IP.
This is a sensible default. Private IPs like 10.x.x.x or 172.16.x.x
are almost certainly load balancers or internal proxies, not real clients.
The Heroku setup
On Heroku, the request path looks like this:
Client → Heroku Router → Rails app
Heroku’s router sets X-Forwarded-For to the client’s IP and connects to
your app from a private IP. So Rails sees something like:
X-Forwarded-For: 99.61.165.29
REMOTE_ADDR: 10.1.45.23
Rails builds a list by combining X-Forwarded-For with REMOTE_ADDR, giving
it [99.61.165.29, 10.1.45.23]. It walks this list right-to-left:
10.1.45.23 is private, skip it. 99.61.165.29 is public — that’s the
client. Correct!
The Render setup
Render uses Cloudflare in front of all its services. So the request path is different:
Client → Cloudflare → Render proxy → Rails app
Now the headers look like this:
X-Forwarded-For: 99.61.165.29, 104.22.17.40
REMOTE_ADDR: 10.21.157.68
104.22.17.40 is a Cloudflare edge IP. It’s a real, public IP address.
Rails walks right-to-left: 10.21.157.68 is private, skip it.
104.22.17.40 is public — Rails thinks this is the client. Wrong.
The client’s actual IP (99.61.165.29) gets ignored.
Cloudflare does set headers with the real client IP — True-Client-Ip
and Cf-Connecting-Ip both contain 99.61.165.29 — but Rails’
request.remote_ip doesn’t look at those headers.
The fix
Since Cloudflare always sits in front of Render, you can read the
True-Client-Ip header that Cloudflare sets. This header contains the
real client IP as determined by Cloudflare’s edge — it can’t be spoofed
by the client.
def client_ip_from_rails_request(rails_request)
rails_request.headers["True-Client-Ip"] ||
rails_request.headers["Cf-Connecting-Ip"] ||
rails_request.remote_ip
end
This is what Render’s own support team
recommends.
They also note that Render’s proxy does not strip incoming
X-Forwarded-For headers — it just appends to them, making
X-Forwarded-For trivially spoofable.
The fallback to remote_ip means this works in development too, where
Cloudflare isn’t in the picture.