---
title: "Your IIS Logs Start Lying the Moment Cloudflare Goes Live"
canonical: https://dxdev.com/blog/iis-logs-lie-after-cloudflare-cf-connecting-ip/
datePublished: 2026-05-12
---
I SSH'd into the production box and read the top of an actual IIS log file, because that was the only honest way to find the bug. The `#Fields:` directive named every column in order, and column 6, the one the log-analysis script had been reading for weeks, was `c-ip`: the Cloudflare edge IP, on every single row. Every "top offender IP" report since the nameservers had moved was actually a ranked list of Cloudflare datacenters. The scrapers we actually wanted to rate-limit were sitting invisible behind them.

The moment your nameservers flip to Cloudflare, every IP your origin sees becomes a Cloudflare edge IP. Your log analyzer, your bot-detection, and the "here's your IP address" page your support team reads to a confused customer all keep working. They just quietly start reporting a Cloudflare datacenter instead of the actual human, and nothing throws an error.

That is the whole trap. A wrong IP is still a perfectly valid-looking IP. There is no exception, no 500, no red line in a log. The flip happened, the records kept flowing, and for a while I had no idea that everything downstream of `REMOTE_ADDR` had started lying.

This is the postmortem of finding it, on a Windows/IIS/classic-ASP sports SaaS that has accumulated years of code and operational assumptions, after orange-clouding the whole site.

## The flip

We moved the nameservers to the two Cloudflare-assigned `*.ns.cloudflare.com` hosts and orange-clouded everything: www, apex, and the media subdomain. From that second on, the origin web server (IIS) no longer talks to browsers. It talks to Cloudflare's edge. The browser's real IP arrives in an HTTP header, `CF-Connecting-IP`, but only if something on your side actually reads it.

We knew that going in, sort of. An IPv6 hotfix earlier in the same release cycle had already taught the application layer to read the right header. Several ASP files got an IP resolution helper with a clear precedence:

```
CF_PSEUDO_IPV4  →  CF_CONNECTING_IP  →  REMOTE_ADDR
```

Cloudflare hands you a synthetic IPv4 (`CF-Pseudo-IPv4`) for IPv6 visitors so legacy code that assumes a v4 string does not choke. If that is present, use it. Otherwise use the real connecting IP. Only fall back to `REMOTE_ADDR` (which is now always a Cloudflare edge) when nothing better exists. The database layer that logged client IPs got the same treatment. As far as the application was concerned, the IP was correct again.

The problem is that "the application" was not the only thing reading client IPs.

## What the hotfix never touched

The app got fixed. The ops scripts did not, because nobody told them the world had changed. Two of them mattered:

- A traffic-watch script that tails IIS traffic for live monitoring
- A log-analysis script that produces the "top offender IP" reports we use to size rate limits and spot scrapers

These do not read HTTP headers through ASP. They read the raw IIS log files on disk. And an IIS log file has a fixed column order. If your code reaches for a hardcoded column index, it does not care what header arrives. It grabs whatever is sitting in that position.

I found the real bug the only honest way: SSH to the production box and read the top of an actual log file. IIS W3C logs carry a `#Fields:` directive that names every column in order. Reading it told the whole story:

- Column 6 (counting from zero, the way the script indexed) is `c-ip`, the connecting IP. Post-flip, that is the Cloudflare edge IP, every single time.
- Column 13 is `CF-Connecting-IP`, the real human, captured as a custom logged header.

The log-analysis script had been reading column 6 since the flip. So every "top offender IP" report it had generated since the nameservers moved was a ranked list of Cloudflare datacenters. The scrapers, the swarm, the abusive ASN you actually wanted to rate-limit, all of them were collapsed behind a handful of CF edge IPs and rendered invisible. The report still ran and still produced a clean, sorted, confident table of garbage.

Nothing alerted on this because column 6 was never empty and never malformed. It held a real, routable, valid IPv4 address the entire time. It was just the wrong one.

## The fix, and the part that bites you twice

The application precedence wanted three things in order: pseudo-IPv4, then connecting-IP, then remote-addr. The logs only had two of those columns. So the first move was to add `CF-Pseudo-IPv4` as a new logged field. On Windows that is an IIS Enhanced Logging change, applied per site with `appcmd` / the `WebAdministration` module, across the three customer-facing sites: the main site, the media subdomain, and the client-SSL site.

That landed `CF-Pseudo-IPv4` as column 14. Then the analyzer got pointed at column 13 (the connecting IP), with column 14 preferred when present. An IPv4 visitor shows `-` in the pseudo column and falls back correctly to the real connecting IP, and that `-` is its own little tell that the field is wired right, since the pseudo column only fills in for IPv6 visitors.

Here is the part that will catch you. A new IIS log field does not take effect retroactively, and it does not take effect the instant you save the config. The new column only appears when the worker process recycles or the log rotates at midnight UTC. So: force the application pools to recycle, then watch for a second `#Fields:` directive to appear mid-file with 15 columns instead of 14. IIS writes a fresh header line every time logging config changes under a running file, so you literally see the schema change show up partway down the log. That is the verification. Not "I saved the setting," but "I forced the recycle and watched the new header land."

## Why this is silent

Every failure mode in this story is silent for the same reason. The wrong IP is structurally identical to the right IP. It is four octets. It routes. It geolocates. It sorts. Every consumer downstream of it, the log parser, the rate-limit sizing, the bot report, the "show me my IP" page, accepts it without complaint because there is nothing to complain about. You do not get an exception when you read the wrong column. You get a plausible answer, which is far more dangerous than an error, because a plausible answer survives your spot checks.

A type error would have caught itself. A null would have caught itself. A valid-but-wrong value is the one class of bug that sails straight through.

The same week, a second surface bit us. Once Cloudflare sits in front of an app that calls itself over HTTPS, internal traffic starts traversing the public edge too. Our iCal calendar generator fetched a schedule endpoint over public HTTPS, which now meant origin to Cloudflare and back to origin. Reading the IIS logs to size a rate limit, the production server's own IP showed up as one of the heaviest "clients," hitting one endpoint over 19,000 times in two hours. Same root cause, different surface. Before you trust an IIS report behind Cloudflare, check three things: reload the `#Fields:` directive after any logging change and confirm the new columns actually landed, confirm your analyzer reads `CF-Connecting-IP` and `CF-Pseudo-IPv4` ahead of `c-ip`, and scan for your own origin's address showing up as a top "client," which means the app is calling itself back through the public edge.

## Related

- [Cloudflare Pro is a box of levers, not a managed service](/cloudflare-pro-tools-to-configure-not-managed): what you must configure yourself after going behind Cloudflare
- [Your CDN's default bot protection is blocking Googlebot and quietly bleeding your SEO](/verified-bot-skip-rule-cloudflare-blocking-googlebot): another silent Cloudflare misconfiguration with no error signal
- [UTC logs, a local clock, and the canary request: timezone discipline in an incident](/canary-request-utc-logs-incident-timing): the discipline required to trust your logs during an incident
- [The Cloudflare-for-SaaS + IIS Gotcha Nobody Documents: SNI vs Host Header](/cloudflare-saas-iis-sni-host-mismatch-gotcha): another IIS+Cloudflare integration surprise
- [The detector was watching the wrong door: a 49,000-request swarm hid on the uninstrumented IIS site](/detector-watching-wrong-iis-site-multi-site-blind-spot): another IIS multi-site observability blind spot
