Get the Real Client IP: CF-Connecting-IP, X-Forwarded-For, X-Real-IP and True-Client-IP
Short answer
Behind a proxy or CDN, REMOTE_ADDR holds the proxy’s address, and the visitor’s IP arrives in a header the proxy sets: CF-Connecting-IP on Cloudflare, True-Client-IP on Akamai, and X-Forwarded-For or X-Real-IP on most load balancers and nginx. Clients can forge these headers, so trust them only on requests that come from your own proxy, and in X-Forwarded-For use the right-most address that is not one of your proxies.
On this page
- Why REMOTE_ADDR shows the proxy’s IP
- Client IP headers by CDN, load balancer and proxy
- X-Forwarded-For: how to find the real client IP safely
- Configure nginx and Apache to restore the client IP
- Get the client IP in Node.js, Python, PHP and Go
- Test that a fake client IP header is ignored
- Look up the client IP
Which header carries the visitor’s IP depends on the provider, and a client that reaches your server directly can forge any of them. This guide lists the header each CDN and cloud sends, shows how to pick the right address from X-Forwarded-For, and gives trusted-proxy settings for nginx, Apache, Node.js, Python, PHP and Go. More guides are on the developer hub.
Why REMOTE_ADDR shows the proxy’s IP
REMOTE_ADDR is the address at the other end of the TCP connection. A proxy ends the visitor’s connection and opens its own, so REMOTE_ADDR becomes the proxy: 127.0.0.1 for a reverse proxy on the same host, a private address such as 10.0.1.23 for a cloud load balancer, or a public edge address for a CDN.
The visitor’s IP survives only in a header that the proxy adds, and any client can send any header. So read the header only when REMOTE_ADDR belongs to a proxy you trust, and block every path around that proxy.
| Stack | Connecting peer | Client IP once trusted proxies are set |
|---|---|---|
| nginx | $remote_addr; after setup $realip_remote_addr | $remote_addr |
| Apache | %{c}a | %a and REMOTE_ADDR |
| PHP | $_SERVER['REMOTE_ADDR'] | $_SERVER['REMOTE_ADDR'] after the web server fix; Laravel $request->ip(); Symfony $request->getClientIp() |
| Node.js, Express | req.socket.remoteAddress | req.ip |
| Django | request.META["REMOTE_ADDR"] | Middleware below |
| Flask, FastAPI | request.remote_addr, request.client.host | The same, with ProxyFix or Uvicorn’s trusted proxies |
Go net/http | r.RemoteAddr (IP:port) | Middleware below |
Client IP headers by CDN, load balancer and proxy
Accept each header only from the provider’s addresses, or it proves nothing.
| Provider | Header with the client IP | Format | Accept it only from |
|---|---|---|---|
| Cloudflare | CF-Connecting-IP; on Enterprise also True-Client-IP | One IP | Cloudflare’s ranges, plus Authenticated Origin Pulls or Cloudflare Tunnel |
| Akamai | True-Client-IP with Send True Client IP Header on (name configurable) | One IP; keep Allow Clients To Set True Client IP Header off | Origin IP ACL or Site Shield |
| Fastly | Fastly-Client-IP | One IP; clients can set it unless your VCL overwrites it | api.fastly.com/public-ip-list |
| AWS Application Load Balancer | X-Forwarded-For | List, client last; optional ip:port | Security group admitting only the load balancer |
| Amazon CloudFront | X-Forwarded-For, or CloudFront-Viewer-Address via an origin request policy | List, viewer last; ip:port | Prefix list com.amazonaws.global.cloudfront.origin-facing and its IPv6 twin |
| Google Cloud Application Load Balancer | X-Forwarded-For | …,<client-ip>,<load-balancer-ip>: client second from the right | 130.211.0.0/22 and 35.191.0.0/16 (global and classic) |
| Azure Front Door | X-Azure-SocketIP, X-Azure-ClientIP, X-Forwarded-For | One IP; use X-Azure-SocketIP, since users can overwrite X-Azure-ClientIP | Service tag AzureFrontDoor.Backend plus an X-Azure-FDID check |
| Vercel | x-real-ip, x-forwarded-for | One IP; Vercel overwrites client values | Not needed |
| Netlify | context.ip in Functions and Edge Functions | One IP | Not needed |
Warning
An IP allowlist proves that a request passed through the provider, not through your account: Cloudflare and Azure Front Door share their ranges among all customers, and a Cloudflare Worker can send any CF-Connecting-IP to hosts outside Cloudflare. Add Authenticated Origin Pulls or Cloudflare Tunnel on Cloudflare, and the X-Azure-FDID check on Front Door.
CF-Connecting-IP and the True-Client-IP header
CF-Connecting-IP comes with every proxied request and always holds one address. X-Forwarded-For gets the same value when the request had none; otherwise Cloudflare appends the connecting address. Cloudflare’s HTTP headers reference recommends CF-Connecting-IP for that reason, or True-Client-IP, which differs only in name, needs an Enterprise plan and matches the header Akamai sends. With Pseudo IPv4 set to Overwrite Headers, IPv6 visitors get a pseudo IPv4 address in both headers and the real one in CF-Connecting-IPv6. If the header never arrives, check that the Remove visitor IP headers Managed Transform is off.
X-Real-IP vs X-Forwarded-For vs Forwarded
| X-Real-IP | X-Forwarded-For | Forwarded | |
|---|---|---|---|
| Standard | No, an nginx convention | No, a de facto convention | Yes, RFC 7239 |
| Value | One address | Comma-separated list, one entry per hop | for=192.0.2.60;proto=https per hop; IPv6 quoted: for="[2001:db8::17]:4711" |
| Forgeable | Yes, unless your proxy overwrites it | The left part, always | The left part, always |
nginx’s real IP module reads X-Real-IP by default; CloudFront strips it from incoming requests. The X-Forwarded-For reference covers the syntax of both list headers, ports, IPv6 and what each load balancer and CDN writes.
X-Forwarded-For: how to find the real client IP safely
Each proxy appends the address it received the request from, and the client can write anything before the first proxy sees the header. Only the entries on the right, added by your own proxies, can be trusted. MDN’s X-Forwarded-For reference describes two safe ways to choose, both reading from the right:
- Trusted proxy list (recommended). Walk the list from right to left, skip every address that belongs to one of your proxies, and take the first one that does not. This is the right-most untrusted address.
- Trusted proxy count. If exactly N proxies append to the header, the client is the Nth entry from the right. It breaks when a request can take a shorter path.
A visitor at 203.0.113.7 sends a forged header through Cloudflare and an AWS load balancer:
Visitor sends X-Forwarded-For: 198.51.100.66
Cloudflare forwards X-Forwarded-For: 198.51.100.66,203.0.113.7
Load balancer forwards X-Forwarded-For: 198.51.100.66,203.0.113.7, 172.70.1.10
Your app sees REMOTE_ADDR 10.0.1.23 (the load balancer)Trusting the VPC and Cloudflare’s ranges, the app skips 10.0.1.23 and 172.70.1.10 (a Cloudflare edge) and stops at 203.0.113.7. Code that takes the left-most entry returns the forged 198.51.100.66.
Parsing rules:
- Join all
X-Forwarded-Forheaders in order before splitting on commas. - Trim spaces and strip ports: Azure Application Gateway, and AWS load balancers with client port preservation, send
203.0.113.7:4711or[2001:db8::7]:4711. - Compare parsed addresses, not strings.
2001:db8::1and2001:0db8:0:0:0:0:0:1are the same address, and::ffff:203.0.113.7is an IPv4 client on a dual-stack socket. - Reject entries that are not IP addresses: Express and Uvicorn pass them through as the client address, and Google warns that earlier values may contain other characters, including spaces.
- Rate-limit IPv6 by prefix, such as the /64, since a client can switch addresses inside it at will.
Configure nginx and Apache to restore the client IP
Fix it in the web server, and every app, log and PHP script behind it sees the visitor’s IP.
nginx: set_real_ip_from, real_ip_header and X-Forwarded-For
The ngx_http_realip_module replaces $remote_addr with the address from a header, but only for requests from addresses in set_real_ip_from. Most packaged builds include it; check with nginx -V 2>&1 | grep -o with-http_realip_module.
Behind Cloudflare, build the list from Cloudflare’s published ranges and rerun the script from cron, as the ranges can change:
#!/bin/sh
# Writes /etc/nginx/conf.d/cloudflare-realip.conf and reloads nginx.
set -eu
v4=$(curl -fsS https://www.cloudflare.com/ips-v4/)
v6=$(curl -fsS https://www.cloudflare.com/ips-v6/)
conf=/etc/nginx/conf.d/cloudflare-realip.conf
{
echo "# Cloudflare ranges, generated $(date -u +%Y-%m-%d)"
for net in $v4 $v6; do echo "set_real_ip_from $net;"; done
echo "real_ip_header CF-Connecting-IP;"
} > "$conf.tmp"
mv "$conf.tmp" "$conf"
nginx -t && nginx -s reloadBehind a load balancer that sends X-Forwarded-For, trust its subnet and let nginx search the list from the right:
set_real_ip_from 10.0.0.0/16; # your load balancer subnet
real_ip_header X-Forwarded-For;
real_ip_recursive on;With real_ip_recursive off (the default), nginx takes the last address in the header. With on, it takes the last address that is not in set_real_ip_from: the right-most untrusted rule. If a CDN sits in front of the load balancer, add the CDN’s ranges too.
Pass X-Real-IP to your app with proxy_set_header
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}After the real IP module has run, $remote_addr is the visitor, so X-Real-IP gives the app one clean value and overwrites whatever the client sent. $proxy_add_x_forwarded_for appends $remote_addr to the incoming list; on a server that faces the internet directly, use proxy_set_header X-Forwarded-For $remote_addr; to drop client entries. In log_format, $remote_addr now logs the visitor and $realip_remote_addr the proxy.
Apache: mod_remoteip with RemoteIPHeader
Run a2enmod remoteip, then:
# Behind Cloudflare
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /etc/apache2/cloudflare-ips.txt
# Behind your own load balancer in a private subnet, use instead:
# RemoteIPHeader X-Forwarded-For
# RemoteIPInternalProxy 10.0.0.0/16Create the list file from Cloudflare’s ranges:
{ curl -fsS https://www.cloudflare.com/ips-v4/; echo; curl -fsS https://www.cloudflare.com/ips-v6/; echo; } > /etc/apache2/cloudflare-ips.txtmod_remoteip reads the header from right to left and stops at the first untrusted address. RemoteIPTrustedProxy never accepts a private address from the header as the client, which suits public CDNs; RemoteIPInternalProxy does, which suits load balancers in your network. In LogFormat, %a logs the visitor and %{c}a the proxy. Entries with a port, such as 203.0.113.7:4711, are not accepted, so remove ports at the load balancer.
Caddy, Traefik and HAProxy
Caddy reads X-Forwarded-For from the left by default, so pair trusted_proxies with trusted_proxies_strict (Caddy 2.8+). Traefik trusts X-Forwarded-* only from forwardedHeaders.trustedIPs. For HAProxy behind Cloudflare, Cloudflare’s guide sets X-Forwarded-For from CF-Connecting-IP for requests from its ranges.
TCP load balancers and the PROXY protocol
Layer 4 load balancers, such as an AWS Network Load Balancer, add no HTTP headers: they keep the client’s source address or send it in a PROXY protocol header. nginx reads that header with listen 443 ssl proxy_protocol;, set_real_ip_from <load-balancer-subnet>; and real_ip_header proxy_protocol;, Apache 2.4.31 and later with RemoteIPProxyProtocol On.
Get the client IP in Node.js, Python, PHP and Go
If you cannot change the web server, or the app sits directly behind the load balancer, configure trust in the framework. For the local and public IP of the machine your code runs on, see the guides for JavaScript, Python and PHP.
Node.js and Express: get the client IP with req.ip
import express from "express";
import { isIP } from "node:net";
const app = express();
// nginx on the same host, plus a load balancer in 10.0.0.0/8
app.set("trust proxy", ["loopback", "10.0.0.0/8"]);
// req.ip is copied from the header as is: drop "::ffff:" and reject anything that is not an IP
function clientIp(req) {
const ip = req.ip?.startsWith("::ffff:") && isIP(req.ip.slice(7)) === 4 ? req.ip.slice(7) : req.ip;
return isIP(ip ?? "") ? ip : null;
}
app.get("/ip", (req, res) => {
res.json({ ip: clientIp(req), chain: req.ips, peer: req.socket.remoteAddress });
});
app.listen(3000);With addresses or subnets, Express starts at the socket address, walks X-Forwarded-For from right to left, and req.ip is the first address it does not trust. Named subnets are loopback, linklocal and uniquelocal. Because Express copies the entry as is, clientIp() drops the ::ffff: prefix and rejects ports and other text.
Warning
Trust-everything settings are safe only when nothing but your proxy can reach the app. Express trust proxy: true and Uvicorn --forwarded-allow-ips="*" both return the left-most X-Forwarded-For entry, which the client controls. Laravel trustProxies(at: '*') and Symfony’s REMOTE_ADDR value trust whoever connects.
Next.js, Vercel and Netlify
Next.js 15 removed request.ip, and Next.js 16 renamed middleware.ts to proxy.ts. Read the address from the platform:
// app/api/ip/route.ts on Vercel
import { ipAddress } from "@vercel/functions";
export function GET(request: Request) {
return Response.json({ ip: ipAddress(request) ?? null });
}ipAddress() reads x-real-ip; x-vercel-forwarded-for keeps the value when another proxy sits in front of Vercel. Self-hosted behind nginx, read request.headers.get("x-real-ip") and bind Next.js to 127.0.0.1 so only nginx can set it. On Netlify, Functions and Edge Functions get context.ip.
Cloudflare Workers: read CF-Connecting-IP
export default {
async fetch(request) {
const ip = request.headers.get("CF-Connecting-IP");
return Response.json({ ip, country: request.cf?.country, asn: request.cf?.asn });
},
};Cloudflare sets the header at its edge, so a Worker can rely on it for the incoming request.
Django: REMOTE_ADDR behind a proxy
Django has no setting for this; add a middleware:
# myproject/middleware.py
import ipaddress
from django.conf import settings
def parse_ip(value):
"""Parse '203.0.113.7', '203.0.113.7:4711', '2001:db8::7' or '[2001:db8::7]:4711'."""
value = value.strip()
if value.startswith("["):
value = value[1:].split("]", 1)[0]
elif value.count(":") == 1:
value = value.split(":", 1)[0]
try:
ip = ipaddress.ip_address(value)
except ValueError:
return None
return getattr(ip, "ipv4_mapped", None) or ip
class ClientIPMiddleware:
"""Set request.client_ip to the right-most address that is not a trusted proxy."""
def __init__(self, get_response):
self.get_response = get_response
self.trusted = [ipaddress.ip_network(n) for n in settings.TRUSTED_PROXIES]
def is_trusted(self, ip):
return any(ip in net for net in self.trusted)
def __call__(self, request):
ip = parse_ip(request.META.get("REMOTE_ADDR", ""))
if ip is not None and self.is_trusted(ip):
for hop in reversed(request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")):
candidate = parse_ip(hop)
if candidate is None:
break
ip = candidate
if not self.is_trusted(ip):
break
request.client_ip = str(ip) if ip is not None else None
return self.get_response(request)In settings.py, set TRUSTED_PROXIES = ["127.0.0.1/32", "::1/128", "10.0.0.0/8"], put "myproject.middleware.ClientIPMiddleware" first in MIDDLEWARE, and read request.client_ip.
Flask: Werkzeug ProxyFix
from flask import Flask, request
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
# One proxy (nginx) in front of the app sets X-Forwarded-For and X-Forwarded-Proto
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
@app.get("/ip")
def ip():
return {"ip": request.remote_addr}x_for is the number of proxies that append to X-Forwarded-For. ProxyFix counts and never checks who connected, so bind the app server to 127.0.0.1 or an interface only the proxy can reach.
FastAPI and Starlette: Uvicorn forwarded-allow-ips
uvicorn main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips="127.0.0.1,10.0.0.0/8"Uvicorn applies X-Forwarded-For only to connections from --forwarded-allow-ips (default 127.0.0.1,::1, or the FORWARDED_ALLOW_IPS environment variable) and sets request.client.host to the right-most untrusted entry; CIDR ranges work. Validate the value with ipaddress.ip_address() before use.
PHP: $_SERVER[‘REMOTE_ADDR’] behind a proxy
With nginx and PHP-FPM, fastcgi_params passes REMOTE_ADDR from $remote_addr; under Apache it follows mod_remoteip. Once the web server is set up, $_SERVER['REMOTE_ADDR'] holds the visitor’s IP. Reading $_SERVER['HTTP_X_FORWARDED_FOR'] or $_SERVER['HTTP_CF_CONNECTING_IP'] unchecked is the classic spoofing bug. If you cannot change the server:
<?php
// Your proxies. Behind Cloudflare, add Cloudflare's ranges and call client_ip('HTTP_CF_CONNECTING_IP').
const TRUSTED_PROXIES = ['127.0.0.1/32', '::1/128', '10.0.0.0/8'];
function ip_in_cidr(string $ip, string $cidr): bool
{
[$net, $bits] = explode('/', $cidr) + [1 => null];
$ip = inet_pton($ip);
$net = inet_pton($net);
if ($ip === false || $net === false || strlen($ip) !== strlen($net)) {
return false;
}
$bits = $bits === null ? strlen($ip) * 8 : (int) $bits;
$mask = str_repeat("\xff", intdiv($bits, 8));
if ($bits % 8) {
$mask .= chr((0xff << (8 - $bits % 8)) & 0xff);
}
$mask = str_pad($mask, strlen($ip), "\0");
return ($ip & $mask) === ($net & $mask);
}
function is_trusted_proxy(string $ip): bool
{
foreach (TRUSTED_PROXIES as $cidr) {
if (ip_in_cidr($ip, $cidr)) {
return true;
}
}
return false;
}
function client_ip(string $header = 'HTTP_X_FORWARDED_FOR'): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if (!is_trusted_proxy($ip)) {
return $ip; // not from your proxy: ignore all headers
}
foreach (array_reverse(explode(',', $_SERVER[$header] ?? '')) as $hop) {
$hop = trim($hop);
if (filter_var($hop, FILTER_VALIDATE_IP) === false) {
break;
}
$ip = $hop;
if (!is_trusted_proxy($hop)) {
break;
}
}
return $ip;
}Laravel and Symfony: trusted proxies
In Laravel 13, list your proxies in bootstrap/app.php and read $request->ip():
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: [
'10.0.0.0/8', // your load balancer subnet
]);
})In Symfony 8, set them in config/packages/framework.yaml and read $request->getClientIp():
framework:
trusted_proxies: '127.0.0.1,10.0.0.0/8'
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port']Both return the right-most X-Forwarded-For address that is not a trusted proxy.
Go: RemoteAddr and a trusted-proxy middleware
r.RemoteAddr holds the peer’s IP:port. ClientIP() applies the right-most untrusted rule:
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/netip"
"strings"
)
var trustedProxies = []netip.Prefix{
netip.MustParsePrefix("127.0.0.0/8"),
netip.MustParsePrefix("::1/128"),
netip.MustParsePrefix("10.0.0.0/8"), // your load balancer subnet
}
func isTrusted(a netip.Addr) bool {
for _, p := range trustedProxies {
if p.Contains(a) {
return true
}
}
return false
}
// parseHop accepts "203.0.113.7", "203.0.113.7:4711", "2001:db8::7" and "[2001:db8::7]:4711".
func parseHop(s string) (netip.Addr, bool) {
s = strings.TrimSpace(s)
if ap, err := netip.ParseAddrPort(s); err == nil {
return ap.Addr().Unmap(), true
}
a, err := netip.ParseAddr(s)
return a.Unmap(), err == nil
}
// ClientIP returns the peer address if it is not a trusted proxy, otherwise the
// right-most X-Forwarded-For entry that is not a trusted proxy.
func ClientIP(r *http.Request) netip.Addr {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
ip, ok := parseHop(host)
if !ok || !isTrusted(ip) {
return ip
}
hops := strings.Split(strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",")
for i := len(hops) - 1; i >= 0; i-- {
hop, ok := parseHop(hops[i])
if !ok {
break
}
ip = hop
if !isTrusted(hop) {
break
}
}
return ip
}
func main() {
http.HandleFunc("/ip", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, ClientIP(r))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}Test that a fake client IP header is ignored
Add a temporary /ip endpoint like the ones above and send forged headers with curl, using a documentation address such as 198.51.100.66:
# 1. Through the CDN or load balancer: expect your own public IP
curl -s https://www.example.com/ip \
-H "X-Forwarded-For: 198.51.100.66" -H "X-Real-IP: 198.51.100.66" -H "CF-Connecting-IP: 198.51.100.66"
# 2. Straight to the origin (replace 203.0.113.10 with its IP): expect a blocked connection
# -k because origin certificates are often not publicly trusted
curl -sk --resolve www.example.com:443:203.0.113.10 https://www.example.com/ip \
-H "CF-Connecting-IP: 198.51.100.66"
# 3. Same over IPv6
curl -6 -s https://www.example.com/ip -H "X-Forwarded-For: 2001:db8::66"If 198.51.100.66 or 2001:db8::66 comes back, your trusted list is too wide or the origin is reachable around the proxy. Then remove the endpoint.
Look up the client IP
With iplookupapi, a trusted address becomes a country, city, network or time zone: send it to the info endpoint, with the key in the IPLOOKUPAPI_KEY environment variable. In Express, with clientIp() from above:
const FIELDS = "ip,location.country.alpha2,location.city.name,connection.asn,connection.organization";
async function lookupIp(ip) {
const url = new URL("https://api.iplookupapi.com/v1/info");
url.searchParams.set("ip", ip);
url.searchParams.set("fields", FIELDS);
const res = await fetch(url, {
headers: { apikey: process.env.IPLOOKUPAPI_KEY },
signal: AbortSignal.timeout(3000),
});
if (!res.ok) throw new Error(`iplookupapi returned HTTP ${res.status}`);
return (await res.json()).data;
}
app.get("/hello", async (req, res) => {
const ip = clientIp(req);
const info = ip ? await lookupIp(ip).catch(() => null) : null;
res.json({
country: info?.location?.country?.alpha2 ?? null,
city: info?.location?.city?.name ?? null,
asn: info?.connection?.asn ?? null,
});
});In Flask, after ProxyFix:
import os
import requests
FIELDS = "ip,location.country.alpha2,location.city.name,connection.asn,connection.organization"
def lookup_ip(ip):
resp = requests.get(
"https://api.iplookupapi.com/v1/info",
params={"ip": ip, "fields": FIELDS},
headers={"apikey": os.environ["IPLOOKUPAPI_KEY"]},
timeout=3,
)
resp.raise_for_status()
return resp.json()["data"]
@app.get("/hello")
def hello():
try:
info = lookup_ip(request.remote_addr)
except requests.RequestException:
info = {}
location = info.get("location") or {}
return {
"country": (location.get("country") or {}).get("alpha2"),
"city": (location.get("city") or {}).get("name"),
}Tip
Cache results per IP for a few hours, since every lookup counts toward your monthly quota. In local development the client IP is 127.0.0.1 or a private address, which has no location.
The SDKs for JavaScript, Python and PHP wrap the same call. To enrich logs later, send up to 1,000 addresses per request to the batch IP lookup API. Under the GDPR an IP address is usually personal data, so keep raw IPs only as long as you need them and store the derived country or network where that is enough.
Frequently asked questions
What is the difference between CF-Connecting-IP and True-Client-IP?
Only the name. Cloudflare documents that both headers carry the same visitor IP. CF-Connecting-IP is sent to the origin on every plan, while True-Client-IP is an Enterprise-only Managed Transform, useful when your origin already expects Akamai’s header name. Akamai sends True-Client-IP when Send True Client IP Header is enabled in the Origin Server behavior.
Should my app read X-Real-IP or X-Forwarded-For?
Read X-Real-IP when your own nginx sets it with proxy_set_header X-Real-IP $remote_addr after its real IP module has restored the visitor, because it then holds one clean address. Otherwise read X-Forwarded-For from the right and take the first address that is not one of your proxies. Accept either header only on connections from your proxy, since clients can send both with any value.
Can CF-Connecting-IP be spoofed?
Yes, if your origin accepts connections from anywhere, because anyone can send that header straight to your server’s IP. Allow only Cloudflare’s IP ranges at the firewall and trust the header only from them. Those ranges are shared by all Cloudflare customers, so Cloudflare recommends Authenticated Origin Pulls or Cloudflare Tunnel to prove that a request came through your own zone.
Which IP in X-Forwarded-For is the real client IP?
Read the list from right to left and skip every address that belongs to a proxy you run or pay for. The first address that is not one of your proxies is the client IP. The left-most entry is whatever the client sent, so never use it for rate limiting or access control. On Google Cloud Application Load Balancers the last entry is the load balancer itself, so the client is the second entry from the right.
Why does REMOTE_ADDR show 127.0.0.1 or a 10.x address?
Your app runs behind a reverse proxy on the same host (127.0.0.1) or a load balancer in a private network (for example 10.0.0.0/8), so the TCP connection comes from that proxy. Set the trusted-proxy option of your web server or framework, such as set_real_ip_from in nginx or trust proxy in Express, and REMOTE_ADDR or req.ip shows the visitor’s address.
How do I get the client IP address in JavaScript?
In Node.js with Express, set app.set(’trust proxy’, …) to the addresses of your proxies and read req.ip. Without a proxy, req.socket.remoteAddress is the client. Browser JavaScript has no API that returns the public IP, so let your server read the address and return it, for example from a small /ip endpoint.
Look up IP addresses in your code
iplookupapi returns the location, provider, time zone and security flags of any IP address as JSON. The free plan includes 150 requests a month.
curl "https://api.iplookupapi.com/v1/info?ip=8.8.8.8" \
-H "apikey: YOUR_API_KEY"