- by x32x01 ||
If your NestJS application sits behind Cloudflare, Caddy, a load balancer, or another reverse proxy, req.ip may not be the actual user's IP.
The problem is usually not the GeoIP database or the IP lookup library. The real issue is that your application is looking at the wrong layer of the network.
For example, your production request path might look like this:
Browser → Cloudflare → Caddy → NestJS
The NestJS server is not directly connected to the browser. It is connected to Caddy. Caddy is connected to Cloudflare, and Cloudflare is the layer that directly receives the user's request.
That distinction matters for everything from GeoIP lookups to rate limiting and security logs.
The key rule is simple: never trust a forwarded IP just because it appears in a header. Trust it only when it comes from a proxy you actually trust.
Why req.ip Can Be Wrong Behind a Reverse Proxy
When a browser connects directly to your backend, the server can usually identify the remote peer from the network connection.
But with a reverse proxy, the connection looks different.
Suppose the real user has this IP:
The request goes through: Browser → Cloudflare → Caddy → NestJS
When NestJS receives the connection, the immediate network peer may be Caddy rather than the browser.
So if your application simply takes the socket's remote address and assumes it is the client IP, you may get the proxy's IP instead of the user's IP.
There is nothing technically broken here.
The TCP connection really was made by Caddy.
The problem is the application's assumption about what that IP represents.
This can produce confusing results when you use the IP for GeoIP lookups. A user in Egypt might suddenly appear to be in another country because you looked up the proxy's address instead of the original client address.
The code can be working perfectly while the input is wrong.
Why You Should Not Blindly Trust X-Forwarded-For
A common solution is to read:
and take an IP from it.
That can work when the proxy chain is configured correctly, but blindly trusting this header creates a security problem.
HTTP headers are client-controlled unless a trusted component in your infrastructure is responsible for setting or sanitizing them.
For example, if your backend is directly reachable by an attacker, they could send:
If your application blindly treats that value as the user's real IP, the attacker can influence the IP your application sees.
That can affect:
The better question is:
"Which proxy is allowed to tell my application what the original client IP was?"
Cloudflare Does Not Automatically Make CF-Connecting-IP Trustworthy
Cloudflare provides the CF-Connecting-IP header to identify the original client IP when traffic passes through Cloudflare.
That makes it useful when Cloudflare is part of your trusted infrastructure.
But the header name itself does not make it trustworthy.
If your origin server can be reached directly, someone may be able to send a request directly to the origin with a forged header such as:
Your application cannot determine from the header name alone whether Cloudflare actually added it.
This is why protecting the origin is an important part of handling client IPs correctly.
If Cloudflare is supposed to be your trusted edge, your infrastructure should be configured so that normal public traffic reaches the origin through the intended path rather than allowing arbitrary clients to bypass that trust boundary.
The Real Problem Is the Trust Chain
Consider this request:
Browser
↓
Cloudflare
↓
Caddy
↓
NestJS
Suppose the real client IP is:
Cloudflare receives the connection directly from the user and therefore knows the original client IP.
Cloudflare then connects to Caddy.
Caddy connects to NestJS.
By the time the request reaches NestJS, the application is several network layers away from the original client.
The application therefore needs a reliable way to reconstruct the original client IP from information supplied by trusted infrastructure.
This is why simply choosing the first or last IP in a forwarded header is not a universal solution.
IP Headers Represent a Proxy Chain
With multiple proxies, X-Forwarded-For can contain several addresses.
For example:
Those addresses can represent different points in the request path depending on how each proxy is configured.
If your infrastructure looks like this:
Browser
↓
Cloudflare
↓
Load Balancer
↓
Caddy
↓
NestJS
then the application may receive information representing several proxy hops.
The important point is that you should not simply decide:
"The first IP is always the user."
or:
"The last IP is always the user."
The correct interpretation depends on which proxies are trusted and how the chain is configured.
Configure Trust at the Application Layer
If NestJS is using Express, the Express trust proxy configuration is particularly important.
It determines which proxies Express considers trusted when calculating values such as:
and:
For example, blindly enabling proxy trust without understanding your network topology can allow clients to influence the address Express considers to be the original client.
The correct configuration depends on your actual deployment.
Your application should know something like:
Browser → Cloudflare → Caddy → NestJS
rather than assuming:
Browser → NestJS
If you add another load balancer later, the trust model may need to change as well.
The same principle applies when NestJS uses Fastify. Its proxy-related configuration should match the actual infrastructure rather than being enabled blindly.
Do Not Build the Logic Around a Fixed Header Priority
A tempting implementation looks like this conceptually:
The problem is that this turns IP detection into a guessing game.
It essentially says:
"If I find an IP-looking value in a header, I will probably trust it."
That is not a trust model.
A better architecture is:
Why This Matters for Rate Limiting
GeoIP is usually the first place where this problem becomes visible, but it is not the most serious consequence.
Imagine your application implements rate limiting per IP.
If NestJS sees the proxy's IP instead of the real client IP, thousands of users could appear to come from the same address.
One user could then consume a shared rate limit and unintentionally affect other users.
The opposite problem can also happen.
If your application blindly trusts a client-controlled forwarded header, an attacker could change the IP on every request.
For example:
Then:
If your rate limiter trusts those values without validating the proxy chain, the attacker may effectively bypass an IP-based limit.
So you can end up with rate-limiting code that looks correct but provides little real protection.
Your Production, Staging, and Local Environments May Be Different
Another easy mistake is assuming every environment has the same network path.
Production might use: Browser → Cloudflare → Caddy → NestJS
Staging might use: Browser → Caddy → NestJS
Local development might use: Browser → NestJS
If the application assumes Cloudflare is always present, the same IP-detection logic may behave differently across environments.
Your IP handling should therefore be based on the actual infrastructure for each environment.
This is especially important when testing security features locally. A configuration that makes sense behind several trusted proxies may be unnecessary or incorrect when the application is running directly.
A Practical Mental Model
Instead of thinking: "I need to find the real IP from a header."
think: "I need to identify the original client through a trusted chain of proxies."
That small change in thinking makes the architecture much easier to reason about.
For every request, ask:
Final Takeaway
Getting the user's IP behind Cloudflare, Caddy, and NestJS is not simply a matter of choosing between CF-Connecting-IP, X-Forwarded-For, and req.ip.
The real issue is trust.
Your backend needs to know which proxies are trusted, how the request travels through them, and whether clients can bypass that chain.
Once the infrastructure and proxy trust configuration are correct, the application can safely use the resulting client IP for GeoIP lookups, rate limiting, security logs, fraud detection, and other IP-based features.
The most dangerous approach is not having no IP detection at all.
It is having IP detection that looks correct, works without errors, and quietly trusts the wrong source.
Frequently Asked Questions
Why does NestJS show the proxy IP instead of the user's IP?
Because NestJS is receiving the network connection from the reverse proxy, not directly from the user's browser. The application needs a correctly configured trusted proxy chain to determine the original client IP.
Is X-Forwarded-For safe to trust?
Not by itself. It should only be trusted when the request has passed through proxies you trust and the origin cannot be bypassed in a way that allows attackers to inject or manipulate the forwarded IP information.
Can I always use CF-Connecting-IP with Cloudflare?
It is useful when Cloudflare is your trusted edge, but the header should not be treated as trustworthy merely because of its name. Your origin and proxy configuration must prevent untrusted clients from spoofing the same information.
Should I always take the first IP from X-Forwarded-For?
No. The correct IP depends on your proxy chain and which proxies are trusted. With multiple proxies, you need to understand how each layer adds and forwards client IP information.
Why is an incorrect client IP a security problem?
Because IP addresses are often used for rate limiting, audit logs, fraud detection, access restrictions, suspicious activity detection, and other security decisions. Using the wrong or attacker-controlled IP can make these controls unreliable.
The problem is usually not the GeoIP database or the IP lookup library. The real issue is that your application is looking at the wrong layer of the network.
For example, your production request path might look like this:
Browser → Cloudflare → Caddy → NestJS
The NestJS server is not directly connected to the browser. It is connected to Caddy. Caddy is connected to Cloudflare, and Cloudflare is the layer that directly receives the user's request.
That distinction matters for everything from GeoIP lookups to rate limiting and security logs.
The key rule is simple: never trust a forwarded IP just because it appears in a header. Trust it only when it comes from a proxy you actually trust.
Why req.ip Can Be Wrong Behind a Reverse Proxy
When a browser connects directly to your backend, the server can usually identify the remote peer from the network connection.
But with a reverse proxy, the connection looks different.
Suppose the real user has this IP:
197.40.x.xThe request goes through: Browser → Cloudflare → Caddy → NestJS
When NestJS receives the connection, the immediate network peer may be Caddy rather than the browser.
So if your application simply takes the socket's remote address and assumes it is the client IP, you may get the proxy's IP instead of the user's IP.
There is nothing technically broken here.
The TCP connection really was made by Caddy.
The problem is the application's assumption about what that IP represents.
This can produce confusing results when you use the IP for GeoIP lookups. A user in Egypt might suddenly appear to be in another country because you looked up the proxy's address instead of the original client address.
The code can be working perfectly while the input is wrong.
Why You Should Not Blindly Trust X-Forwarded-For
A common solution is to read:
X-Forwarded-Forand take an IP from it.
That can work when the proxy chain is configured correctly, but blindly trusting this header creates a security problem.
HTTP headers are client-controlled unless a trusted component in your infrastructure is responsible for setting or sanitizing them.
For example, if your backend is directly reachable by an attacker, they could send:
HTTP:
X-Forwarded-For: 8.8.8.8 That can affect:
- Rate limiting
- Security logs
- Login history
- Fraud detection
- Country restrictions
- Suspicious activity detection
- Audit logs
- IP-based access controls
The better question is:
"Which proxy is allowed to tell my application what the original client IP was?"
Cloudflare Does Not Automatically Make CF-Connecting-IP Trustworthy
Cloudflare provides the CF-Connecting-IP header to identify the original client IP when traffic passes through Cloudflare.
That makes it useful when Cloudflare is part of your trusted infrastructure.
But the header name itself does not make it trustworthy.
If your origin server can be reached directly, someone may be able to send a request directly to the origin with a forged header such as:
HTTP:
CF-Connecting-IP: 197.40.x.x This is why protecting the origin is an important part of handling client IPs correctly.
If Cloudflare is supposed to be your trusted edge, your infrastructure should be configured so that normal public traffic reaches the origin through the intended path rather than allowing arbitrary clients to bypass that trust boundary.
The Real Problem Is the Trust Chain
Consider this request:
Browser
↓
Cloudflare
↓
Caddy
↓
NestJS
Suppose the real client IP is:
197.40.x.xCloudflare receives the connection directly from the user and therefore knows the original client IP.
Cloudflare then connects to Caddy.
Caddy connects to NestJS.
By the time the request reaches NestJS, the application is several network layers away from the original client.
The application therefore needs a reliable way to reconstruct the original client IP from information supplied by trusted infrastructure.
This is why simply choosing the first or last IP in a forwarded header is not a universal solution.
IP Headers Represent a Proxy Chain
With multiple proxies, X-Forwarded-For can contain several addresses.
For example:
HTTP:
X-Forwarded-For: 197.40.x.x, 203.0.113.10, 198.51.100.20 If your infrastructure looks like this:
Browser
↓
Cloudflare
↓
Load Balancer
↓
Caddy
↓
NestJS
then the application may receive information representing several proxy hops.
The important point is that you should not simply decide:
"The first IP is always the user."
or:
"The last IP is always the user."
The correct interpretation depends on which proxies are trusted and how the chain is configured.
Configure Trust at the Application Layer
If NestJS is using Express, the Express trust proxy configuration is particularly important.
It determines which proxies Express considers trusted when calculating values such as:
req.ipand:
req.ipsFor example, blindly enabling proxy trust without understanding your network topology can allow clients to influence the address Express considers to be the original client.
The correct configuration depends on your actual deployment.
Your application should know something like:
Browser → Cloudflare → Caddy → NestJS
rather than assuming:
Browser → NestJS
If you add another load balancer later, the trust model may need to change as well.
The same principle applies when NestJS uses Fastify. Its proxy-related configuration should match the actual infrastructure rather than being enabled blindly.
Do Not Build the Logic Around a Fixed Header Priority
A tempting implementation looks like this conceptually:
CF-Connecting-IP → otherwise X-Forwarded-For → otherwise req.ipThe problem is that this turns IP detection into a guessing game.
It essentially says:
"If I find an IP-looking value in a header, I will probably trust it."
That is not a trust model.
A better architecture is:
- Identify every proxy in the request path.
- Decide which proxies are trusted.
- Make sure untrusted clients cannot bypass those proxies.
- Configure each proxy to pass the client IP correctly.
- Configure the application framework to trust only the expected proxies.
- Use the resulting client IP consistently throughout the application.
Why This Matters for Rate Limiting
GeoIP is usually the first place where this problem becomes visible, but it is not the most serious consequence.
Imagine your application implements rate limiting per IP.
If NestJS sees the proxy's IP instead of the real client IP, thousands of users could appear to come from the same address.
One user could then consume a shared rate limit and unintentionally affect other users.
The opposite problem can also happen.
If your application blindly trusts a client-controlled forwarded header, an attacker could change the IP on every request.
For example:
HTTP:
X-Forwarded-For: 1.1.1.1 HTTP:
X-Forwarded-For: 2.2.2.2 So you can end up with rate-limiting code that looks correct but provides little real protection.
Your Production, Staging, and Local Environments May Be Different
Another easy mistake is assuming every environment has the same network path.
Production might use: Browser → Cloudflare → Caddy → NestJS
Staging might use: Browser → Caddy → NestJS
Local development might use: Browser → NestJS
If the application assumes Cloudflare is always present, the same IP-detection logic may behave differently across environments.
Your IP handling should therefore be based on the actual infrastructure for each environment.
This is especially important when testing security features locally. A configuration that makes sense behind several trusted proxies may be unnecessary or incorrect when the application is running directly.
A Practical Mental Model
Instead of thinking: "I need to find the real IP from a header."
think: "I need to identify the original client through a trusted chain of proxies."
That small change in thinking makes the architecture much easier to reason about.
For every request, ask:
- Who connected directly to my application?
- Which proxy connected to that server?
- Which proxy received the request from the previous layer?
- Which components are trusted?
- Can a client bypass those components?
- Which component is responsible for supplying the original client IP?
- Does the framework know which proxies it should trust?
Final Takeaway
Getting the user's IP behind Cloudflare, Caddy, and NestJS is not simply a matter of choosing between CF-Connecting-IP, X-Forwarded-For, and req.ip.
The real issue is trust.
Your backend needs to know which proxies are trusted, how the request travels through them, and whether clients can bypass that chain.
Once the infrastructure and proxy trust configuration are correct, the application can safely use the resulting client IP for GeoIP lookups, rate limiting, security logs, fraud detection, and other IP-based features.
The most dangerous approach is not having no IP detection at all.
It is having IP detection that looks correct, works without errors, and quietly trusts the wrong source.
Frequently Asked Questions
Why does NestJS show the proxy IP instead of the user's IP?
Because NestJS is receiving the network connection from the reverse proxy, not directly from the user's browser. The application needs a correctly configured trusted proxy chain to determine the original client IP.
Is X-Forwarded-For safe to trust?
Not by itself. It should only be trusted when the request has passed through proxies you trust and the origin cannot be bypassed in a way that allows attackers to inject or manipulate the forwarded IP information.
Can I always use CF-Connecting-IP with Cloudflare?
It is useful when Cloudflare is your trusted edge, but the header should not be treated as trustworthy merely because of its name. Your origin and proxy configuration must prevent untrusted clients from spoofing the same information.
Should I always take the first IP from X-Forwarded-For?
No. The correct IP depends on your proxy chain and which proxies are trusted. With multiple proxies, you need to understand how each layer adds and forwards client IP information.
Why is an incorrect client IP a security problem?
Because IP addresses are often used for rate limiting, audit logs, fraud detection, access restrictions, suspicious activity detection, and other security decisions. Using the wrong or attacker-controlled IP can make these controls unreliable.
