Client IP Address in PHP: How to Avoid Mistakes in 2025

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

The Hunt for IP: Why Just $_SERVER['REMOTE_ADDR'] Isn't Good Enough Anymore?

\n\n

Imagine: you launched a cool educational project in PHP. Everything is flying, users are happy. But then you decide to add visitor logging, geographic targeting, or, say, protection against vote manipulation. And then — bam! — it turns out that half of your "unique" visitors are sitting with the same IP: 127.0.0.1 or 10.0.0.1. Sound familiar?

\n\n

The problem of getting the real client IP address in PHP is a classic that never loses relevance. The question on Stack Overflow gets thousands of views, and beginners (and not only) keep stepping on the same rake. In 2025, when Cloudflare, AWS, Nginx, and a bunch of CDN proxies are the de facto standard, relying on $_SERVER['REMOTE_ADDR'] is like reading tea leaves.

\n\n

In this article, we won't just give you a ready-made function. We'll break down the anatomy of HTTP headers, learn to distinguish friend from foe, and write a secure and reliable way to get a visitor's IP address in PHP that won't let you down in 2025. Let's go!

\n\n

REMOTE_ADDR: Your Faithful, But Blind Dog

\n\n

Let's start with the basics. $_SERVER['REMOTE_ADDR'] is the IP address of whoever directly connected to your web server. If your site is on a dedicated server without a proxy, this is the client's IP. Simple and reliable.

\n\n

But in the modern world, this is rarely the case. More often, there is a whole chain between the user and your PHP script:

\n
    \n
  • CDN (Cloudflare, Fastly)
  • \n
  • Load balancer (Nginx, HAProxy)
  • \n
  • Reverse proxy (Varnish, Nginx)
  • \n
  • Shared hosting architecture
  • \n
\n\n

And here, REMOTE_ADDR shows the IP of the last link (e.g., your load balancer), not the real user. For an educational site, this is a disaster: you won't be able to distinguish a student from Moscow from a student from New York, and the anti-spam system will block everyone indiscriminately.

\n\n

HTTP Headers: Where Does the Real IP Hide?

\n\n

To pass the real client IP, proxy servers add special HTTP headers. The most popular ones in 2025 are:

\n\n

1. X-Forwarded-For (XFF)

\n

A classic header. It can contain a chain of IPs: client_ip, proxy1_ip, proxy2_ip. The first IP in the list is the real client. But there's a catch: this header can be forged by any attacker if your infrastructure doesn't clean it.

\n\n

2. X-Real-IP

\n

Nginx's favorite header. Contains only one IP — the real one. Simpler and more secure, but not supported by all proxies.

\n\n

3. CF-Connecting-IP (Cloudflare)

\n

A Cloudflare-specific header. If you use their CDN, this is the most reliable option. But only if you trust Cloudflare (and you should, they're good).

\n\n
\n Important: Never trust X-Forwarded-For and X-Real-IP headers from unknown sources. An attacker can spoof any IP, and your system will consider it real.\n
\n\n

Best Practice 2025: Multi-Layer Protection

\n\n

So what to do? We can't just take the first IP from X-Forwarded-For — that's dangerous. But we can't ignore proxies either. The solution is a whitelist of trusted proxies.

\n\n

The idea is simple: we know the IP addresses of our load balancers and CDNs. If the request came from them, we trust the headers. If not, we use REMOTE_ADDR.

\n\n

Here's a working PHP class that implements this logic:

\n\n
<?php\n\nclass ClientIP\n{\n    /**\n     * List of trusted IP addresses and subnets (CIDR).\n     * Add the IPs of your proxies, CDNs, and load balancers here.\n     */\n    private static array $trustedProxies = [\n        '127.0.0.1',          // localhost\n        '10.0.0.0/8',         // private networks\n        '172.16.0.0/12',                

Blogs

Book Recommendations