How to Get a Client IP Address in Node.js (Six Ways in 2026)
Learn six reliable ways to read a client IP in Node.js, Express, reverse proxies, Cloudflare and Forwarded headers—without trusting spoofed values.
Direct answer: In a plain Node.js HTTP server, read req.socket.remoteAddress. It is the address of the network peer directly connected to Node.js. If your app is behind a reverse proxy, load balancer or CDN, that peer is usually the intermediary. In Express, use req.ip after configuring trust proxy to match your real deployment.
Forwarded headers are claims made by proxies. They are useful only when requests can reach your application through a trusted, correctly configured proxy chain. Never treat an arbitrary X-Forwarded-For value as an authenticated user identity.
1. Plain Node.js: read req.socket.remoteAddress
This is the simplest and most reliable method when Node.js is directly exposed or when you specifically need the immediate TCP peer.
const http = require('node:http');
const server = http.createServer((req, res) => {
const peerAddress = req.socket.remoteAddress;
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ peerAddress }));
});
server.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
remoteAddress can be an IPv4 address, an IPv6 address, or an IPv4-mapped IPv6 value such as ::ffff:192.0.2.10. Do not assume it is always a public IPv4 address. Local development commonly produces ::1 or 127.0.0.1.
Node’s HTTP API documents this socket property as the remote address of the connection peer: Node.js HTTP documentation.
2. Express without a trusted proxy
Express exposes req.ip. With the default trust proxy setting disabled, Express derives it from the socket peer, so it behaves like req.socket.remoteAddress.
const express = require('express');
const app = express();
app.get('/ip', (req, res) => {
res.json({ ip: req.ip, ips: req.ips });
});
app.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
req.ips is normally empty when proxy trust is disabled. This configuration is appropriate when clients connect directly to the application and no upstream service supplies trusted client metadata.
3. Express behind a known proxy topology
When a reverse proxy or load balancer terminates the client connection, configure trust proxy. Express then evaluates the socket address and the X-Forwarded-For chain, stopping at the first untrusted address. req.ip is the client address Express selected; req.ips contains the remaining chain.
Trust a proxy subnet or address
const express = require('express');
const app = express();
// Replace these example networks with the private ranges used by your proxy.
app.set('trust proxy', ['loopback', '10.0.0.0/8', '192.168.0.0/16']);
app.get('/ip', (req, res) => {
res.json({ ip: req.ip, ips: req.ips });
});
app.listen(3000);
Use the actual addresses or subnets of the proxy tier. A custom function is useful when trust depends on an allowlist maintained by your infrastructure:
const trustedProxies = new Set(['127.0.0.1', '10.20.0.5']);
app.set('trust proxy', (ip) => trustedProxies.has(ip));
Trust a hop count only when topology is fixed
// Trust exactly one proxy hop.
app.set('trust proxy', 1);
A hop count is safe only when every route to the app has the same number of hops. If one path has fewer proxies, a client may place a forged address where Express expects a trusted hop. Express explains the proxy behavior and these risks in its behind proxies guide.
Why trust proxy: true is risky
true trusts forwarded information supplied by the nearest proxy. It is safe only when the final trusted proxy always overwrites or removes incoming forwarding headers and the app cannot be reached around that proxy. Otherwise a client can influence the address your application sees.
4. Parse X-Forwarded-For in a custom Node.js handler
X-Forwarded-For is a comma-separated chain. Proxies may append addresses, and clients can send the header themselves. Read it directly only after you have established that traffic arrives through trusted proxies that sanitize or append it correctly.
const http = require('node:http');
const trustedProxyAddresses = new Set(['10.0.0.5', '10.0.0.6']);
function firstUntrustedFromRight(req) {
const socketAddress = req.socket.remoteAddress;
const values = (req.headers['x-forwarded-for'] || '')
.split(',')
.map(value => value.trim())
.filter(Boolean);
// Work from the server side toward the client.
const chain = [...values, socketAddress].filter(Boolean).reverse();
for (const address of chain) {
if (!trustedProxyAddresses.has(address)) return address;
}
return socketAddress;
}
http.createServer((req, res) => {
res.end(firstUntrustedFromRight(req) || 'unknown');
}).listen(3000);
The leftmost value is often described as the original client, but it can be user supplied when the first proxy does not overwrite the header. MDN’s X-Forwarded-For reference recommends determining trust from the server-side end of the chain and considering multiple header fields together.
For rate limits, access control and audit decisions, use only an address introduced by infrastructure you control. Even then, the first trustworthy address may identify an intermediate proxy rather than the user’s device.
5. Parse the standardized Forwarded header
Forwarded is a separate standard from X-Forwarded-For. Its grammar supports parameters, quoted values and IPv6 forms such as for="[2001:db8::1]:1234". It is not safe to parse it as a simple comma-separated IP list.
const forwarded = req.headers.forwarded;
if (typeof forwarded === 'string') {
// Pass this to a standards-aware Forwarded parser and validate
// the result against your trusted proxy topology.
console.log(forwarded);
}
Use a maintained parser that understands the structured grammar, then apply the same trust policy used for XFF. See MDN’s Forwarded reference.
6. Use a provider-specific header such as Cloudflare’s
When the origin is reachable only through Cloudflare, Cloudflare documents CF-Connecting-IP and True-Client-IP for restoring the visitor address. Cloudflare may append to an existing XFF chain; for a request with no existing XFF, XFF matches CF-Connecting-IP.
function cloudflareClientIp(req) {
const value = req.headers['cf-connecting-ip'];
return typeof value === 'string' ? value : req.socket.remoteAddress;
}
app.get('/ip', (req, res) => {
res.json({ ip: cloudflareClientIp(req) });
});
Protect the origin so clients cannot bypass Cloudflare and forge this header. True-Client-IP must be enabled in the Cloudflare setup. Consult Cloudflare’s HTTP header reference and True-Client-IP documentation.
Choosing the right method
| Deployment | Use | What it represents | Security condition |
|---|---|---|---|
| Direct Node.js | req.socket.remoteAddress |
Immediate TCP peer | No proxy interpretation required |
| Direct Express | req.ip with default settings |
Socket peer | App is directly exposed |
| Known reverse proxy | req.ip plus subnet-based trust proxy |
First address outside trusted chain | Proxy addresses and header behavior are known |
| Custom XFF | Parse from right to left | First untrusted address | All proxy hops sanitize or append correctly |
| Forwarded | Standards-aware parser | Structured proxy claim | Parser and proxy trust policy are configured |
| Cloudflare origin | CF-Connecting-IP or True-Client-IP |
Cloudflare’s client-address value | Origin accepts traffic only from Cloudflare |
IPv4, IPv6 and normalization
- Accept IPv6 values; do not split on colons to detect an address.
- Expect loopback forms such as
::1in local development. - Normalize IPv4-mapped IPv6 values only for display or comparison, using a tested IP library.
- Validate syntax with a maintained IP parser before storing or applying network rules.
- Never use an IP as a durable user identity. Addresses can be shared, rotated, proxied or anonymized.
Privacy and security checklist
- Document which proxy adds each forwarding header.
- Block direct internet access to the origin when relying on provider headers.
- Prefer explicit proxy subnets or addresses over blanket trust.
- Do not use a fixed hop count on paths with variable length.
- For security controls, use only the first address outside the trusted chain.
- Limit retention and access because IP addresses expose network-location information.
- Log the socket peer and the selected client address separately while diagnosing proxy issues.
Complete Express example with safe proxy configuration
const express = require('express');
const app = express();
// Example only: replace with your actual ingress ranges.
app.set('trust proxy', ['10.0.0.0/8', 'loopback']);
app.get('/diagnostics/ip', (req, res) => {
res.json({
selectedIp: req.ip,
proxyChain: req.ips,
socketPeer: req.socket.remoteAddress,
xForwardedFor: req.headers['x-forwarded-for'] || null,
forwarded: req.headers.forwarded || null
});
});
app.listen(process.env.PORT || 3000);
Keep a diagnostic route access-controlled in production. Returning raw forwarding headers to everyone can disclose infrastructure details.
Troubleshooting
req.ip shows the load balancer or proxy
Cause: Proxy trust is disabled, or the proxy address is not in the trusted set.
Fix: Configure trust proxy with the proxy’s real subnet or address. Confirm that the proxy overwrites or appends forwarding headers consistently.
req.ip is a client-controlled value
Cause: The app trusts every proxy or is reachable directly from the internet.
Fix: Restrict origin access, trust only known proxy addresses, and inspect the socket peer before accepting forwarded values.
Different requests produce different hop counts
Cause: Some routes pass through a CDN and others do not, or internal and public paths have different lengths.
Fix: Replace numeric hop-count trust with an address or subnet policy that matches every ingress path.
Cloudflare’s header is missing
Cause: The request bypassed Cloudflare, the origin is not configured behind Cloudflare, or the header was removed upstream.
Fix: Lock down the origin to Cloudflare traffic, verify the zone configuration and fall back to the socket peer only for explicitly direct traffic.
IPv6 breaks validation
Cause: Code assumes dotted-quad IPv4 syntax or splits an address on :.
Fix: Use an IP parser that supports IPv4 and IPv6, and store the normalized result consistently.
Multiple XFF headers are handled incorrectly
Cause: Infrastructure may combine, preserve or overwrite repeated fields differently.
Fix: Inspect the raw behavior of your proxy chain and apply one documented parsing policy. MDN advises considering all XFF fields together.
Performance and reliability notes
Reading req.socket.remoteAddress or a header is constant-time work compared with the network request itself. The reliability challenge is configuration: an incorrect trust policy can silently select the wrong address. Keep proxy ranges in configuration, review them when infrastructure changes, and add integration checks through every ingress path.
For rate limiting, keying solely by client IP can penalize users behind carrier-grade NAT or a corporate proxy. Combine the selected address with authenticated identity or another application-level key where appropriate. For abuse prevention, treat forwarded addresses as signals, not proof.
Or skip the browser setup
If your Node.js workflow also needs screenshots of pages associated with requests, ScreenshotNeo provides a single API call instead of maintaining a browser stack. It removes cookie banners, newsletter popups and chat widgets before capture. Bot checks, blank pages and failed loads are never billed, and responses identify the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor and other MCP clients take screenshots.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes 1,000 screenshots a month free with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Does remoteAddress identify the human user?
No. It identifies the direct network peer. A proxy, NAT gateway or CDN may be that peer.
Should I always use X-Forwarded-For?
No. Use it only when a trusted proxy chain is guaranteed and its behavior is documented.
Is an IP address suitable as a user ID?
No. IPs can be shared, reassigned and hidden. Use an authenticated account or session identifier for identity.
Which Express setting is safest?
A setting that names the actual trusted proxy addresses or subnets. Numeric hop counts and blanket trust require stricter topology guarantees.
Can I support both Cloudflare and direct traffic?
Yes, but make the two paths explicit. Accept Cloudflare headers only from Cloudflare’s networks and use the socket peer for verified direct traffic.


