Hey guys, it’s me again — back with another story about IP addresses and networking stuff. This time I want to share a quick anecdote about how I was incorrectly handling IP logging in one of my old Node.js/Express side projects that I deployed on Heroku (back when they still had a free tier… RIP 🥀).
So the project had a simple middleware that looked like this:
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('Client IP:', req.socket.remoteAddress);
next();
});
Pretty straightforward, right?
Well… not quite.
When I checked the logs, instead of seeing real client IP addresses, all I saw were internal Heroku server IPs. At the time, I had absolutely no idea why.
The “aha!” moment: Heroku uses reverse proxies
What I didn’t know back then is that Heroku automatically places your app behind load balancers and reverse proxies.
Unlike something like a raw AWS EC2 instance — where traffic hits your machine directly — Heroku as most of modern cloud hosting providers intercepts all inbound traffic first.
Here’s roughly what I thought was happening:
User → Your Express App
But here’s what actually happens:
User
|
v
[ Heroku Load Balancer / Reverse Proxy ]
|
v
Your Express App
So when Express reads:
req.socket.remoteAddress
…it’s not showing the user’s IP. It’s showing Heroku’s proxy IP, because that’s what is technically connected to your app.
How reverse proxies preserve the real client IP
Reverse proxies work like middlemen. They accept the incoming request, open a separate connection to your server, and then forward everything along.
To make sure the origin server (your app) still knows the real user IP, proxies add a special header:
X-Forwarded-For: <real client IP>
So the request flow looks like this:
User IP 203.0.113.42
|
| (User connects)
v
Heroku Proxy
|
| Adds header: X-Forwarded-For: 203.0.113.42
v
Your Express App
Once I understood that, the fix became easy.
Two ways to fix it in Express
Option 1: Tell Express to trust the proxy
When you enable this, Express automatically uses X-Forwarded-For for req.ip.
const express = require('express');
const app = express();
app.set('trust proxy', true);
app.use((req, res, next) => {
console.log('Client IP:', req.ip);
next();
});
This is the recommended approach for Heroku, Render, Vercel, etc.
Option 2: Read the header manually
If you prefer to handle it yourself:
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('Client IP:', req.headers['x-forwarded-for']);
next();
});
Just note that the header can contain a comma-separated list if multiple proxies are in the chain. Usually, the first entry is the real client.
The takeaway
If your Node/Express app runs behind any cloud hosting, CDN, load balancer, or reverse proxy (Heroku, Vercel, Netlify, AWS ELB, Cloudflare, Nginx, etc.), remember:
| What you’re using | What you get |
|---|---|
req.socket.remoteAddress | ❌ Proxy’s IP (useless) |
req.ip with trust proxy | ✅ Real client IP |
req.headers['x-forwarded-for'] | ✅ Real client IP (manual) |
Bottom line: Always check how your hosting platform handles incoming traffic. Most modern providers use reverse proxies, which means you’ll need to configure your app to read the X-Forwarded-For header — either automatically via trust proxy or manually.
This is one of those things you learn after staring at logs wondering why every single user of your app seems to live inside your hosting provider’s data center. 😅
This is a follow-up to my previous article: I’ve Been a Developer for over 8 Years, and I Just Actually Understood IP Addresses