DMA Codes Explained: Mapping US Visitors to TV Markets
A DMA code is a three-digit Nielsen identifier for one of 210 US television markets: 501 is New York, 803 is Los Angeles, 807 is San Francisco-Oakland-San Jose. Media buyers plan, price, and report local TV by these mark
A DMA code is a three-digit Nielsen identifier for one of 210 US television markets: 501 is New York, 803 is Los Angeles, 807 is San Francisco-Oakland-San Jose. Media buyers plan, price, and report local TV by these markets, and a lot of web and ad stacks now tag visitors with them too.
You can look up a DMA code by IP address. Check a visitor in Philadelphia and you get back "504". Look up a visitor in Toronto or San Juan and you get an empty value. That empty value is the correct answer, not a bug, and most DMA reporting problems start when a pipeline forgets it.
TL;DR
- A DMA (Designated Market Area) is a Nielsen TV market built from whole counties. There are 210, with three-digit codes running from 500 to 881.
- DMAs are US-only. Canada, Puerto Rico, and everywhere else return no code, and common tools encode "no DMA" differently. MaxMindโs similarly named
metro_codeis a deprecated legacy field, not a current Nielsen DMA code. - An IP-derived DMA is only as good as the location estimate under it. Trust it in aggregate, not per person.
- Ad platforms no longer take the bare code: Google Ads wants its own location ID, and Meta switched to Comscore Markets in June 2026.
- Store the Nielsen code as a string, normalize empty to NULL, and translate to platform IDs at the edge. In short: the DMA code is a market label, not a precise location. Pull it from an IP lookup for US traffic, keep "no market" and "lookup failed" as separate states, and treat the Nielsen code as your canonical key, because Google, Meta, and OpenRTB each describe the same New York market with a different identifier.
What a DMA code actually is
Nielsen defines a DMA as a group of counties where the home-market TV stations capture the dominant share of viewing. There are 210 of them. They don't overlap, and together they cover the continental US, Hawaii, and parts of Alaska. Nielsen reviews the county assignments every year and ranks the markets by the number of TV households in each.
The code itself is a three-digit label. Radar's published DMA list starts at 500 (Portland-Auburn) and ends at 881 (Spokane), with gaps along the way, so 210 codes are spread across a range of 382 numbers. It isn't a ZIP code, an area code, or a number you can do math on. 798 is Glendive, Montana; that tells you nothing about 797.
Because markets are built from counties, they ignore state lines. An IP that geolocates to Piscataway, New Jersey comes back as 501, because Nielsen assigns its county, Middlesex, to the New York market.
One more thing before the useful part: the acronym collides with everything. When I checked Google's AI Overview for "dma code" in November 2026, it explained Direct Memory Access cheat hardware for Call of Duty. In this article DMA means the Nielsen media market and nothing else.
US-only, and "US" is narrower than you'd guess
Nielsen's footprint is the continental US, Hawaii, and parts of Alaska. Outside it, there is no DMA to return. Here's what five live DMA code lookups returned on November 3, 2026:
| IP | country_code2 |
Location | dma_code |
|---|---|---|---|
| 107.161.145.197 | US | Philadelphia, PA | "504" |
| 8.8.8.8 | US | Mountain View, CA | "807" |
| 128.6.0.1 | US | Piscataway, NJ | "501" |
| 136.145.0.1 | PR | San Juan, Puerto Rico | "" |
| 99.224.0.1 | CA | Toronto, ON | "" |
The Puerto Rico row is the one that surprises people. It's US territory, but it's not in a Nielsen DMA, and the lookup reports it as country PR, not US. A country_code2 == "US" check already filters it out, which is convenient as long as nobody "fixes" the check to include territories.
The bigger problem is that every system spells "no DMA" differently:
| System | Field | Value when there's no DMA |
|---|---|---|
| IPGeolocation API | location.dma_code |
"" (empty string) |
| Cloudflare Rules | ip.src.metro_code |
null ([typed `String \ |
| Adobe Analytics | US DMA dimension |
{% raw %}"No Metro (0)" for all non-US traffic |
Pick one representation at ingest, and make it NULL. An empty string survives into GROUP BY dma_code as a market of its own, and a zero turns into a bar on a chart labeled "No Metro" that someone in a planning meeting will ask about. And never backfill the nearest US market for a Canadian visitor. That's inventing audience, and it will show up as a strange lift in Buffalo.
How an IP turns into a DMA code
I'll use IPGeolocation for the examples because the DMA is one extra include on the same lookup that returns city, county, and ZIP, so there's no second call. The field comes from the include=dma_code option in IPGeolocation's location API. It needs a paid plan, but it's billed as part of the one-credit location lookup rather than as an add-on.
# Keep the key in an env var; never paste it into scripts you commit.
curl -s "https://api.ipgeolocation.io/v3/ipgeo?apiKey=${IPGEO_API_KEY}&ip=107.161.145.197&include=dma_code&fields=location"
The response, verbatim from a live call:
{
"ip": "107.161.145.197",
"location": {
"continent_code": "NA",
"continent_name": "North America",
"country_code2": "US",
"country_code3": "USA",
"country_name": "United States",
"country_name_official": "United States of America",
"country_capital": "Washington, D.C.",
"state_prov": "Pennsylvania",
"state_code": "US-PA",
"district": "Philadelphia County",
"city": "Philadelphia",
"dma_code": "504",
"zipcode": "19102",
"latitude": "39.95258",
"longitude": "-75.16522",
"is_eu": false,
"country_flag": "https://ipgeolocation.io/static/flags/us_64.png",
"geoname_id": "9849057",
"country_emoji": "๐บ๐ธ"
}
}
fields=location keeps the example short. Drop it and the same call also returns the country metadata, currency, ASN, and time zone objects. Two things in the location object matter here. dma_code is a string, so treat it like one. And district holds the county, which is the unit Nielsen builds markets from. Get the county right and the market follows.
That's also where accuracy lives. The chain runs from IP to estimated location to county to market, so a wrong county means a wrong market, and a county on a market border is the easiest one to get wrong. Adobe's US DMA documentation lists the usual suspects: corporate proxies, mobile carriers that backhaul traffic through regional hubs, satellite ISPs, military networks, and relays like iCloud Private Relay. I treat DMA as an aggregate signal. "Sessions from the Philadelphia market doubled during the TV flight" holds up. "This visitor is in Philadelphia, so show the Philadelphia ad" is a guess with decent odds, not a fact.
Current Nielsen DMA fields are also available from IPinfo, Cloudflare and Adobe Analytics. IPinfo exposes a dma_code string on its paid geolocation plans, Cloudflare provides ip.src.metro_code in its Rules engine, and Adobe fills its US DMA dimension server-side. MaxMind's deprecated metro_code is a legacy Google advertising metro code and should not be used as a current Nielsen DMA mapping.
In a pipeline, I wrap the lookup so three outcomes stay separate: a market, no market, and a failed lookup. Collapsing the last two is how outages end up in reports as "non-US traffic spiked."
import logging
import os
from dataclasses import dataclass
from typing import Optional
import requests
API_URL = "https://api.ipgeolocation.io/v3/ipgeo"
log = logging.getLogger(__name__)
@dataclass(frozen=True)
class DmaResult:
code: Optional[str] # "501", or None
status: str # "matched", "no_dma", or "lookup_failed"
def lookup_dma(ip: str) -> DmaResult:
api_key = os.environ.get("IPGEO_API_KEY")
if not api_key:
raise RuntimeError("IPGEO_API_KEY is not set")
params = {"apiKey": api_key, "ip": ip, "include": "dma_code", "fields": "location"}
try:
resp = requests.get(API_URL, params=params, timeout=(1.0, 1.5))
resp.raise_for_status()
data = resp.json()
except requests.HTTPError as exc:
# Do not log the exception object: it can contain the URL and API key.
status = exc.response.status_code if exc.response is not None else "unknown"
log.warning("DMA lookup failed for %s: HTTP %s", ip, status)
return DmaResult(code=None, status="lookup_failed")
except requests.RequestException as exc:
# Log only the exception type, never the request URL.
log.warning("DMA lookup failed for %s: %s", ip, type(exc).__name__)
return DmaResult(code=None, status="lookup_failed")
except ValueError:
log.warning("DMA lookup returned invalid JSON for %s", ip)
return DmaResult(code=None, status="lookup_failed")
location = data.get("location") if isinstance(data, dict) else None
if not isinstance(location, dict):
log.warning("Unexpected response shape for %s", ip)
return DmaResult(code=None, status="lookup_failed")
code = str(location.get("dma_code") or "").strip()
if not code:
# The API sends "" outside Nielsen's footprint; store a real NULL.
return DmaResult(code=None, status="no_dma")
# Keep it a string. Codes are labels, and other sources send strings too.
return DmaResult(code=code, status="matched")
Store status in a column next to code. Then pass in the client IP your proxy or load balancer reports as trusted, not the raw left-most X-Forwarded-For value, which any client can set.
How ad and local-media teams use the dma field
Reporting web traffic by TV market
Media plans are written in DMAs. If a brand runs local TV or streaming in three markets, the obvious question afterward is whether site traffic and conversions moved in those three markets and not in comparable ones. Tag sessions with dma_code, join on the plan, and the media team can read results in the same 210-market map they bought against.
The same join supports matched-market tests: run a campaign in some DMAs, hold it back in similar ones, and compare. It works because buyer, seller, and analyst are all using one shared geography, which is rarer than it should be.
Local sites and local sales
A station or newsroom site can tell an in-market reader from an out-of-market one and adjust what it promotes, and a regional sales team can report audience by the markets it actually sells. Neither needs per-person precision. Both need the code to be consistent from one report to the next.
Handing the market to an ad platform
This is where the three-digit code stops being enough. Google Ads targets DMA regions through its own location IDs: the New York DMA is GeoTargetConstant 200501, and Google's documentation is blunt that "You cannot pass raw ISO or Nielsen codes."
Meta went further. Effective June 22, 2026, it moved from Nielsen DMAs to Comscore Markets "across ads targeting and reporting solutions." In the Ads Insights API, breakdowns=dma is no longer supported; you request breakdowns=comscore_market instead. Map your Nielsen codes to Meta's Comscore market list explicitly rather than assuming the numbers carry over.
Programmatic is fuzzier again. In OpenRTB 2.6, the bid request's geo.metro field is defined as a "Google metro code; similar to but not exactly Nielsen DMAs."
| Where the market shows up | What New York looks like |
|---|---|
| IP lookup (Nielsen DMA) | 501 |
| Google Ads location targeting |
200501 (GeoTargetConstant) |
| Meta targeting and reporting (since June 22, 2026) | A Comscore market, via breakdowns=comscore_market
|
OpenRTB bid request geo.metro
|
A Google metro code, "not exactly" a Nielsen DMA |
My rule: the Nielsen code is the canonical key in the warehouse, and every platform ID is a translation done at the edge, from one mapping table that one person owns. Teams that let each platform's ID leak into the core tables spend their quarter reconciling dashboards.
The licensing question
Nielsen is explicit that "DMAยฎ boundaries and data are solely owned and exclusive to Nielsen." It sells its ZIP-by-DMA data under tiers, and the reporting-only license "cannot be used for ad targeting, media buying or sales." Reporting on a market and targeting by it can be different questions under a license.
So if DMA targeting is the plan, not just dashboards, read your IP data provider's terms for that use case, and ask if they're unclear. I'm not a lawyer, and this isn't legal advice; it's the one question worth asking before launch rather than after.
Mistakes that make DMA data lie
-
Casting the code to an integer. Nothing breaks today because codes start at 500, but your IP lookup and Cloudflare both send strings, and
"504" != 504in most joins. -
Treating
""or0as a market. See the NULL rule above. -
Trusting an old
metro_codepipeline. If it reads MaxMind's field, it's reading values MaxMind stopped maintaining. - Sending a bare Nielsen code to an ad platform. Google wants 200501-style IDs. Meta wants Comscore markets.
- Caching the DMA forever. Nielsen reviews its county assignments every year, and IP locations move too. Re-resolve on a schedule.
- Reading one visitor's DMA as fact. It's a market estimate for an IP, nothing more. ## A few extra notes
IPv6 works the same way. 2001:4860:4860::8888 returns "807", the same market as 8.8.8.8. The response echoes the address in expanded form (2001:4860:4860:0:0:0:0:8888), so normalize IPs before joining on them.
There's no non-US equivalent hiding in the same field. Other countries measure TV audiences with their own geographies, and none of them are Nielsen DMAs, so an empty dma_code outside the US stays empty.
DMAs and metro areas aren't the same either. A DMA follows TV viewing; a census metro area follows commuting and economics. They often share a name and rarely share a boundary.
If DMA is going into a long-lived table, store the Nielsen code as a string with a status column beside it, and re-resolve when Nielsen publishes a new season. Keep the Google and Comscore translations in a single mapping table, and the numbers will reconcile when the media team asks why the dashboard and the ad platform disagree.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.