Client IP address and geolocation in Application Insights
By default Application Insights uses the client's IP address once, to look up a city, region and country, and then discards it and writes 0.0.0.0 to client_IP. It replaces the whole address, not just the last octet. To store real addresses, set DisableIpMasking to true on the Application Insights resource; it affects only data ingested afterwards, and Microsoft recommends leaving it off. Behind a proxy, server telemetry reads X-Forwarded-For and uses the last address in the list, which may be the proxy rather than the user.
People usually arrive at this question from one of two directions: a security review asking why IP addresses aren’t in the logs, or a dashboard where every user seems to live next to the data centre. Both come down to the same two steps the service performs on every item.
What happens to the address
When telemetry reaches Azure, the ingestion service:
- Looks up the address and fills
client_City,client_StateOrProvinceandclient_CountryOrRegion(ClientCity,ClientStateOrProvinceandClientCountryOrRegionin the workspace). - Discards the address and writes
0.0.0.0toclient_IP(ClientIP).
Microsoft describes this as deliberate, to avoid collecting personal data and location information you didn’t ask for. It’s a full replacement, not the last-octet masking some older blog posts describe. And when no address is collected at all, no city or country is filled in either.
Where the address comes from depends on the telemetry:
- Browser telemetry (the JavaScript SDK): the ingestion endpoint takes the sender’s address from the incoming connection. That’s the user’s public address, or their corporate proxy’s.
- Server telemetry (requests, dependencies and the rest from your back end): the Application Insights module collects the client address of the incoming request, using the
X-Forwarded-Forheader when a proxy sets it. When that list has more than one address, the last one is used for geolocation.
Proxies, gateways and the wrong city
By convention each proxy appends to X-Forwarded-For the address it received the request from, so the list grows by one entry per hop. With a single proxy in front of the app, the last entry is the user. With two (a CDN in front of an application gateway, say), the last entry can be the first proxy, and every user is placed wherever that proxy runs.
When that happens, override the address with the one you trust. Your proxy chain decides which entry that is; a common choice is the address your edge proxy reports in its own header.
Classic SDK 2.x
In the .NET SDK 2.x (deprecated, retiring March 31, 2027) the work is done by ClientIpHeaderTelemetryInitializer, which sets Context.Location.Ip from X-Forwarded-For. To use a different value, add an initializer of your own; Microsoft’s documentation shows one for exactly this purpose:
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Http;
public class TrustedClientIpInitializer : ITelemetryInitializer
{
private readonly IHttpContextAccessor _http;
public TrustedClientIpInitializer(IHttpContextAccessor http) => _http = http;
public void Initialize(ITelemetry telemetry)
{
if (telemetry is not RequestTelemetry request) return;
var ip = _http.HttpContext?.Request.Headers["X-Client-IP"].ToString();
if (!string.IsNullOrEmpty(ip)) request.Context.Location.Ip = ip;
}
}
Register it with builder.Services.AddHttpContextAccessor() and builder.Services.AddSingleton<ITelemetryInitializer, TrustedClientIpInitializer>(). The header name here is an example: use the one your edge proxy sets and strips from incoming requests. The value is used for the lookup and then masked like any other unless masking is disabled. See telemetry initializers for registration in other app types.
The retired Node.js 2.x SDK has no initializers; its telemetry processors receive each envelope and can change it before it is sent.
Today: SDK 3.x and the Microsoft OpenTelemetry Distro
SDK 3.x removed ClientIpHeaderTelemetryInitializer along with the other built-in initializers; in classic ASP.NET its job is done by an internal processor. Location.Ip remains a public context property, so a manually created TelemetryClient or an individual item can still set it:
var request = new RequestTelemetry("GET /orders", DateTimeOffset.Now, TimeSpan.FromMilliseconds(120), "200", true);
request.Context.Location.Ip = clientIp;
telemetryClient.TrackRequest(request);
With OpenTelemetry, the address used for geolocation comes from a span attribute. Which one depends on the language:
| Language | Attribute | Notes |
|---|---|---|
| .NET | client.address | Only on server activities (requests) |
| Node.js | client.address | http.client_ip is accepted as an alternate |
| Python | http.client_ip | Set in a span processor |
| Java | Set automatically | No code needed |
In .NET, set it in an activity processor registered before the distro:
using System.Diagnostics;
using OpenTelemetry;
public class ClientIpProcessor : BaseProcessor<Activity>
{
public override void OnEnd(Activity activity)
{
if (activity.Kind == ActivityKind.Server && TrustedIp.Current is { } ip)
{
activity.SetTag("client.address", ip);
}
}
}
TrustedIp.Current stands for however your app reads the trusted address for the current request. The stored client_IP still follows the masking rule.
One path ignores the switch entirely. Telemetry sent through native OTLP ingestion (an OTLP endpoint into a data collection rule, stored in OTelSpans and OTelLogs) always records 0.0.0.0 in ClientIP, whatever DisableIpMasking says. client.address is still used for the lookup on that path, but the address isn’t kept.
Storing real IP addresses
Microsoft’s default and recommendation is not to collect IP addresses; if you do, check it against your compliance requirements and local law first. The switch is a property of the Application Insights component, DisableIpMasking. The quickest way to set it is Azure PowerShell:
Update-AzApplicationInsights -ResourceGroupName "rg-shop" -Name "appi-shop-prod" -DisableIPMasking
In ARM or Bicep, set DisableIpMasking: true inside the component’s properties. Template deployments and the REST Components - Create Or Update call replace the resource rather than patching it, so keep every other property, including WorkspaceResourceId, in what you send.
Two consequences to plan for: only telemetry ingested after the change shows real addresses (older rows stay 0.0.0.0), and the addresses then live in your workspace under its retention. Microsoft suggests marking the relevant tables as protected tables, which block standard and custom read roles until you grant access explicitly.
Before you switch masking off
Most reasons people give for storing addresses are served by the columns you already have. A map of users by country, a split of failures by region or a check that a CDN is routing Europe to the European origin all work from client_CountryOrRegion and client_City, which are filled with masking on. Real addresses earn their place in narrower cases: investigating abuse from specific sources, or matching telemetry against firewall and gateway logs during an incident.
If you do enable it, decide three things first:
- Who can read it. Protected tables and workspace access control limit the audience; a shared workspace makes every reader an IP reader.
- How long it stays. Table-level retention lets the request table keep addresses for less time than the rest of the workspace.
- What your privacy notice says. Microsoft’s guidance is to consult the privacy rules that apply to you before storing identifiers.
Also remember that location is only as good as the address. Users behind a corporate proxy or VPN appear wherever that network leaves the internet, with masking on or off.
Troubleshooting
| Symptom | Likely cause |
|---|---|
client_IP is always 0.0.0.0 | Default masking; expected unless DisableIpMasking is true |
Still 0.0.0.0 after enabling | Looking at data ingested before the change, or the data came through native OTLP |
| City empty for server telemetry | No client address collected (for example, background work with no incoming request) |
| Everyone in one city | Address of a proxy or load balancer used; fix X-Forwarded-For or set the address yourself |
| Template edit “didn’t save” the property | The exported template never shows it; check with Get-AzResource -ExpandProperties |
To check what a resource is collecting, query the location columns directly:
requests
| where timestamp > ago(1d)
| summarize requests = sum(itemCount) by client_CountryOrRegion, client_City, client_IP
| top 20 by requests
It counts with itemCount because of sampling.
Queries for this topic
-
Requests by client country, with share and p95
requests usage
Returns: One row per client country or region with its sampling-adjusted request count, its share of all non-synthetic requests and its p95 server duration in milliseconds.
-
Page views by page name and country
pageViews usage
Returns: Up to 50 rows, one per page name and country or region, with the sampling-adjusted page view count and the number of distinct users, most viewed first.
Frequently asked questions
Does Application Insights mask only the last octet of the IP address?
No. Microsoft's documentation describes the whole address being discarded after the geolocation lookup, with 0.0.0.0 written to client_IP. Only the city, state or province, and country or region columns keep anything from it.
Can I turn IP collection back off after enabling it?
Yes. Set DisableIpMasking back to false on the resource. Addresses already stored stay in the tables until their retention ends, so plan for that when you enable it, and use protected tables or table-level retention to limit who can read them and for how long.
Why does every user appear to be in the same city as my data centre?
The address used for the lookup is your proxy's or load balancer's, not the user's. Make sure the proxy sets X-Forwarded-For, and remember that the last address in that list is used. Where several proxies append to it, set the client address yourself from the hop you trust.
Sources, checked
- Application Insights geolocation and IP address handling
- Add and modify OpenTelemetry in Application Insights (Set the user IP)
- Monitor .NET and Node.js applications with Application Insights (Classic API 2.x)
- Migration guidance: Application Insights 2.x to 3.x (telemetry context)
- AppRequests table reference