Before a Windows machine can put a packet on the wire for a host on its own subnet, it needs that host’s MAC address. The Address Resolution Protocol asks for it, and Windows keeps the answer in the ARP cache so it does not have to ask again for every frame. The arp command is the built-in way to read that cache and to edit it.
This matters on exactly the problems that the routing table cannot explain. A host that pings from one server and not another, a printer that answers on the wrong port after being moved, an address conflict warning in the System log, a VRRP failover that half the subnet did not notice. All of those are layer 2 questions, and the ARP cache is where the evidence is.
Microsoft’s reference lists the switches and stops there. That leaves the two things people actually get wrong: most of the rows in a typical arp -a listing are not neighbours at all, and a static entry added with arp -s does not survive what an admin assumes it survives. This article covers both, with a cache listing read row by row.
Applies to: Windows 10 / 11, Windows Server 2016 / 2019 / 2022 / 2025
Quick answer
Reading the cache needs no special rights. Run this in any command prompt and you get every cached mapping, grouped by the interface that learned it.
rem the whole cache, one block per interface
arp -a
rem just one address: the fastest way to answer "what MAC is answering for this IP"
arp -a 192.168.120.199
If the address you asked about is not in the cache, make Windows resolve it first. One ping is enough, and it works even when the host drops the ICMP echo, because the ARP exchange happens before the echo request is sent.
ping -n 1 192.168.120.199
arp -a 192.168.120.199
Everything that writes to the cache, which means -s and -d, needs an elevated prompt. Open one with Win+X then “Terminal (Admin)”, or from a normal prompt run the first line below. The second line is a one-shot check that it worked: it prints a match only when the token is running at high integrity.
powershell -Command "Start-Process cmd -Verb RunAs"
rem S-1-16-12288 is the High Mandatory Level SID: present only in an elevated token
whoami /groups | findstr /c:"S-1-16-12288"
What arp does
The cache is per interface, not per machine. Each adapter that speaks ARP keeps its own table, which is why a multi-homed server shows several blocks and why the same address can appear twice with two different MACs. Microsoft’s reference gives three syntax lines, one per mode.
arp -s <inet_addr> <eth_addr> [<if_addr>]
arp -d <inet_addr> [<if_addr>]
arp -a [<inet_addr>] [-N <if_addr>] [-v]
The full switch list is short enough to learn in one sitting.
| Switch | What it does | Needs elevation |
|---|---|---|
-a | Displays the current entries. With an address, shows only that entry. With several ARP interfaces, shows a block for each. | No |
-g | Identical to -a. A leftover from the BSD tool. | No |
-v | Verbose: adds invalid entries and entries on the loopback interface. | No |
-N if_addr | Limits output to the interface holding IP if_addr. Case sensitive, and only valid together with -a. | No |
-d inet_addr | Deletes one entry. * as the address deletes all of them. | Yes |
-s inet_addr eth_addr | Adds a static entry mapping the address to a MAC written as six hex bytes separated by hyphens. | Yes |
if_addr | Optional last argument of -s and -d: the IP of the interface whose table is changed. If omitted, the first applicable interface is used. | n/a |
arp /a and arp /s. Both forms work. This article uses the hyphen form that the current reference and the built-in arp -? help both print.
Reading arp -a output
This is a real cache from a Windows host, posted on Microsoft Q&A by an admin asking why the bottom rows kept coming back after he deleted them. Only the whitespace has been normalised to the column layout arp prints. Eleven rows, one interface.
Interface: 192.168.120.176 --- 0x15
Internet Address Physical Address Type
192.168.120.1 00-09-0f-09-00-03 dynamic
192.168.120.25 d8-d3-85-c2-8a-9a dynamic
192.168.120.26 78-e3-b5-12-45-8c dynamic
192.168.120.27 78-e3-b5-12-d1-84 dynamic
192.168.120.199 c4-17-fe-8d-1a-ea dynamic
192.168.120.255 ff-ff-ff-ff-ff-ff static
224.0.0.2 01-00-5e-00-00-02 static
224.0.0.22 01-00-5e-00-00-16 static
224.0.0.252 01-00-5e-00-00-fc static
239.255.255.250 01-00-5e-7f-ff-fa static
255.255.255.255 ff-ff-ff-ff-ff-ff static
Five of those eleven rows are neighbours. The other six are not hosts at all, and no amount of flushing will remove them for long. Taking the header line first:
| Part of the output | What it actually is |
|---|---|
Interface: 192.168.120.176 | The local IP of the adapter that owns this table, not the adapter name. On a multi-homed host this is how you tell the blocks apart. |
--- 0x15 | The interface index, in hexadecimal. 0x15 is 21, and 21 is the number PowerShell reports as ifIndex. This is the join between arp output and every Net* cmdlet. |
Type: dynamic | Learned from the wire by an ARP exchange, and it will age out. These are the rows worth reading. |
Type: static | Not learned. Either you added it with -s, or it is a broadcast or multicast destination whose MAC Windows derives by formula. |
The six static rows in that listing split into two groups. Two are broadcast: 192.168.120.255 is the subnet broadcast and 255.255.255.255 is the limited broadcast, and both map to the all-ones MAC. The other four are IPv4 multicast groups, and their MACs are not looked up anywhere. They are computed: 01-00-5e followed by the low 23 bits of the group address, which is the mapping defined in RFC 1112.
You can check that claim against the listing above without leaving PowerShell. This function applies the formula, and its output matches all four multicast rows in the cache exactly.
function Get-McastMac {
param([string]$Group)
# low 23 bits: clear the top bit of the second octet, keep the third and fourth as-is
$b = ([ipaddress]$Group).GetAddressBytes()
'01-00-5e-{0:x2}-{1:x2}-{2:x2}' -f ($b[1] -band 0x7f), $b[2], $b[3]
}
'224.0.0.2','224.0.0.22','224.0.0.252','239.255.255.250' |
ForEach-Object { $_.PadRight(16) + (Get-McastMac $_) }
224.0.0.2 01-00-5e-00-00-02
224.0.0.22 01-00-5e-00-00-16
224.0.0.252 01-00-5e-00-00-fc
239.255.255.250 01-00-5e-7f-ff-fa
arp -d * appears to do nothing to them: they are regenerated the moment the stack needs the group again, and there was never a host behind them to re-learn.
Knowing which groups they are also tells you what the machine is doing. These are the ones that show up on almost every Windows subnet.
| Group | Registered use |
|---|---|
| 224.0.0.2 | All routers on this subnet |
| 224.0.0.22 | IGMPv3 membership reports |
| 224.0.0.251 | mDNS |
| 224.0.0.252 | LLMNR |
| 239.255.255.250 | SSDP, used by UPnP and by network discovery |
Practical examples
1. Find which physical device owns an IP
The problem: a monitoring alert names 192.168.120.199 but nobody knows what the box is, and DNS has no record for it.
The solution: resolve the address once, read its MAC out of the cache, then identify the vendor from the first three bytes. The first three bytes are the OUI, the block IEEE assigned to the manufacturer, and they are usually enough to tell a switch from a printer from a hypervisor NIC.
rem the ping is only there to force the ARP exchange; the reply does not matter
ping -n 1 192.168.120.199 >nul
arp -a 192.168.120.199
Interface: 192.168.120.176 --- 0x15
Internet Address Physical Address Type
192.168.120.199 c4-17-fe-8d-1a-ea dynamic
An empty result here is not evidence that the host is down. It means nothing on this machine has needed that MAC recently. Read it as “no answer yet”, ping again, and only then treat silence as a finding.
2. Find every address one device is answering for
The problem: a switch port keeps flapping and you suspect one NIC is claiming more than one address.
The solution: search the cache by MAC instead of by IP. Note the /i: arp prints MACs in lowercase, and the address you copied out of a switch table or an event log is very likely uppercase.
rem /i makes the case of the pasted MAC irrelevant
rem /c: treats the string as a literal, so the hyphens are not read as anything special
arp -a | findstr /i /c:"78-e3-b5"
192.168.120.26 78-e3-b5-12-45-8c dynamic
192.168.120.27 78-e3-b5-12-d1-84 dynamic
Two different MACs from the same vendor block, on two different addresses, is normal. The same full MAC on two addresses is not, and that case is covered further down.
3. Filter the cache down to real neighbours
The problem: on a host with four adapters, arp -a prints forty rows and thirty of them are the same broadcast and multicast placeholders repeated per interface.
The solution: keep only the dynamic rows. Those are the only ones that were learned from another device.
rem "dynamic" appears in no other line of the output: not the header, not the Interface line
arp -a | findstr /c:"dynamic"
Interface: lines with everything else, so on a multi-homed host you can no longer tell which adapter learned which row. When that matters, use the parser in the PowerShell section, which tracks the interface as it reads.
If you need the two useful columns on their own, for a report or to feed another command, take them with for /f. Tokens 1 and 2 are the address and the MAC on every padding width arp produces.
rem run this from a prompt; in a .bat file the variable is %%a, not %a
for /f "tokens=1,2" %a in ('arp -a ^| findstr /c:"dynamic"') do @echo %a %b
4. Search for an address without over-matching
The problem: you grep the cache for one host and get four rows back.
The solution: understand why, then anchor the pattern. findstr matches substrings, and an IPv4 address is a prefix of several others. Searching the eleven-row cache above for 192.168.120.2 returns four lines, because .25, .26, .27 and .255 all contain that string.
rem returns 4 rows: .25, .26, .27 and .255 all start with the string you typed
arp -a | findstr /c:"192.168.120.2"
rem /r switches on regex; the spaces either side anchor the field, and \. is a literal dot
arp -a | findstr /r /c:" 192\.168\.120\.25 "
The second form returns the single row for .25. Simpler still, when you only want one address, skip findstr altogether and let arp do the filtering with arp -a 192.168.120.25.
5. Clear a stale entry after hardware was swapped
The problem: a NIC was replaced in SRV-PROD-01 an hour ago. Most of the subnet reaches it; one server still does not.
The solution: the one server is still holding the old MAC. Delete that entry and let it be re-learned. Both commands need the elevated prompt from the Quick answer.
rem one entry: targeted, and the only form worth using during an incident
arp -d 192.168.120.27
rem confirm it was learned again, with the new MAC this time
ping -n 1 192.168.120.27 >nul
arp -a 192.168.120.27
To flush everything on the machine, arp -d * works, and netsh can do it per adapter by name, which is safer on a server where one interface is carrying storage or cluster traffic.
rem every interface, every entry, including any static ones you added
arp -d *
rem one adapter only, by its name in Network Connections
netsh interface ipv4 delete arpcache name="Ethernet"
arp -d * first. It deletes your static entries along with the stale one, and on a busy host the dynamic rows are back within seconds anyway, so it rarely proves anything that arp -d on the single address would not have proved.
Static entries and where they are stored
Windows documents three ways to pin an address to a MAC, and each one defaults to a different lifetime. This is the same trap the routing table sets, described in the route guide, and it catches people for the same reason: the tool that looks oldest and simplest is the one that forgets.
| Tool | Documented default lifetime | Survives a reboot |
|---|---|---|
arp -s | Removed if the TCP/IP protocol is stopped and started | No |
netsh interface ipv4 add neighbors | store= defaults to persistent | Yes |
New-NetNeighbor | Written to ActiveStore and PersistentStore unless you pick one | Yes |
All three examples below pin the same lab address, 192.168.120.50, to the same MAC, and each one is followed by its removal. Run them on a test host: a static entry that names the wrong MAC will blackhole traffic to that address silently, with no error anywhere.
The classic form is arp -s. Pass the interface IP as the third argument even when the host has one adapter, because the documented behaviour without it is to use “the first applicable interface”, which on a multi-homed machine is not necessarily the one owning that subnet.
rem third argument is the LOCAL interface IP: it decides which table gets the entry
arp -s 192.168.120.50 00-15-5d-2a-7c-04 192.168.120.176
rem it now shows as static under that interface
arp -a 192.168.120.50
rem and this takes it away again
arp -d 192.168.120.50 192.168.120.176
The netsh form takes the adapter by name rather than by address, and writes to the persistent store by default, so it comes back after a restart whether or not you wanted that.
rem store=persistent is the default here; spelling it out makes the intent readable
netsh interface ipv4 add neighbors interface="Ethernet" address=192.168.120.50 neighbor=00-15-5d-2a-7c-04 store=persistent
netsh interface ipv4 show neighbors interface="Ethernet"
netsh interface ipv4 delete neighbors interface="Ethernet" address=192.168.120.50
The PowerShell form is the one to prefer on anything modern, because it is the only one that tells you which store the entry landed in. Get the interface index first: it is the decimal form of the hex number in the arp -a header.
# 0x15 in the arp header is 21 here; confirm which adapter that is before writing
Get-NetIPInterface -InterfaceIndex 21 -AddressFamily IPv4 |
Select-Object ifIndex, InterfaceAlias, ConnectionState
# State Permanent is the only state New-NetNeighbor can create
New-NetNeighbor -InterfaceIndex 21 -IPAddress 192.168.120.50 -LinkLayerAddress '00-15-5d-2a-7c-04' -State Permanent
# the PolicyStore column is the answer to "will this be here after a reboot"
Get-NetNeighbor -IPAddress 192.168.120.50 -PolicyStore PersistentStore
# -WhatIf first: this cmdlet will happily take a whole subnet's entries with one wildcard
Remove-NetNeighbor -InterfaceIndex 21 -IPAddress 192.168.120.50 -WhatIf
-WhatIf from that last line once you have read what it plans to do, and then check Get-NetNeighbor -PolicyStore PersistentStore again rather than trusting arp -a, which shows you the active store only.
Hidden gems
A duplicate IP is proved by two MACs, not by the warning
Windows detects conflicts itself. A DHCP client sends a gratuitous ARP request before it commits to the address it was offered, and the System log records the result under source Tcpip: event 4198 when the local interface is disabled as a result, event 4199 when operations may be disrupted. Both messages carry the other device’s hardware address, which is the part worth keeping.
# pull the conflict events with their full message text, newest first
Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Tcpip'; Id=4198,4199} -MaxEvents 5 |
Select-Object TimeCreated, Id, Message
Now go to a third machine on the same subnet and ask it who is answering. The MAC in its cache is whichever host replied first, so if it differs from the MAC in the event, you have both offenders identified: one from the log, one from the wire.
rem run this from an uninvolved host, not from either machine in the conflict
arp -d 192.168.120.27
ping -n 1 192.168.120.27 >nul
arp -a 192.168.120.27
From there, Resolve-DnsName -Type PTR or a lookup in the switch’s MAC address table turns each MAC into a port and a name. The nslookup guide covers the reverse lookup when DNS is the faster route.
Set-NetIPInterface -DadTransmits 0, and warns that doing so leaves you with duplicate addresses and no warning at all. It is a workaround for switches whose IP Device Tracking feature trips the check, not a fix for a conflict.
One MAC on several addresses is usually innocent
Grouping the cache by MAC finds the suspicious rows in one pass. Exclude the broadcast MAC first, or every subnet broadcast row will be reported as a duplicate of the limited broadcast row.
# State Permanent rows are ones you added yourself, so leave them out of the hunt
Get-NetNeighbor -AddressFamily IPv4 |
Where-Object { $_.State -ne 'Permanent' } |
Group-Object LinkLayerAddress |
Where-Object Count -gt 1
A hit is a starting point, not a verdict. A router doing proxy ARP, a firewall answering for a NAT range, and an HSRP or VRRP pair sharing a virtual MAC all produce exactly this pattern legitimately. What makes it a fault is one MAC holding two addresses that belong to two different servers.
A collection’s .Address property is not the address
This one costs an afternoon if you meet it cold. .NET arrays expose their own Address method, and it wins against PowerShell’s member enumeration. So a property you named Address reads back correctly from a single object and returns a method signature from a collection of two.
$rows = 1..2 | ForEach-Object { [pscustomobject]@{ Address = "192.168.120.$_" } }
$rows.Address # prints a method signature, not addresses
$rows | Select-Object -ExpandProperty Address # prints both addresses
$rows | ForEach-Object { $_.Address } # prints both addresses
The fix that avoids the question entirely is to name the property IPAddress, which is what Get-NetNeighbor calls it. Do that and dot notation works on one row or a hundred.
-N is a filter, not a mode
arp -N 192.168.120.176 on its own prints the help screen, because -N only qualifies -a. It also takes the interface’s IP address, never its name and never the hex index from the output header. The two forms below do the same job.
rem -N narrows -a to one interface, identified by that interface's own IP
arp -a -N 192.168.120.176
rem -v adds invalid entries and the loopback interface: useful when a row you expect is missing
arp -a -v
PowerShell equivalents
Get-NetNeighbor reads the same cache and returns objects, so no parsing is needed. It also shows something arp cannot: the neighbour state, which distinguishes an entry that was confirmed recently from one that is merely remembered.
| Task | CMD | PowerShell |
|---|---|---|
| Read the whole cache | arp -a | Get-NetNeighbor |
| One address | arp -a 192.168.120.199 | Get-NetNeighbor -IPAddress 192.168.120.199 |
| Search by MAC | arp -a | findstr /i /c:"MAC" | Get-NetNeighbor -LinkLayerAddress 'MAC' |
| One interface | arp -a -N 192.168.120.176 | Get-NetNeighbor -InterfaceIndex 21 |
| Add a static entry | arp -s | New-NetNeighbor -State Permanent |
| Delete an entry | arp -d 192.168.120.50 | Remove-NetNeighbor -IPAddress 192.168.120.50 |
The states are worth knowing before you read the output. Reachable means confirmed recently, Stale means the mapping is still held but unverified, Permanent means somebody added it by hand, and Unreachable means resolution was attempted and failed. A cache full of Stale rows is normal, not a symptom.
# Unreachable rows are noise on a busy host; the other three states are the real cache
Get-NetNeighbor -AddressFamily IPv4 -State Reachable,Stale,Permanent |
Sort-Object ifIndex, IPAddress |
Format-Table ifIndex, IPAddress, LinkLayerAddress, State -AutoSize
On an older host where the NetTCPIP module is missing, or when the output has to come from arp for consistency with a script that already exists, parse it. The interface has to be tracked as a variable while the lines stream past, because each Interface: header applies to every row after it until the next one.
$ifIp = $null; $ifIdx = 0
$entries = arp -a | ForEach-Object {
if ($_ -match '^Interface: (\S+) --- (0x[0-9a-f]+)') {
# remember which adapter owns the rows that follow; 0x15 becomes 21
$ifIp = $matches[1]
$ifIdx = [Convert]::ToInt32($matches[2], 16)
}
elseif ($_ -match '^\s+(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f]{2}(?:-[0-9a-f]{2}){5})\s+(\w+)\s*$') {
# the MAC group is non-capturing inside, so Type stays group 3
[pscustomobject]@{
InterfaceIP = $ifIp
ifIndex = $ifIdx
IPAddress = $matches[1]
LinkLayerAddress = $matches[2]
Type = $matches[3]
}
}
}
# real neighbours only: drop the broadcast rows and the 224.0.0.0/4 multicast rows
$entries | Where-Object {
$_.LinkLayerAddress -ne 'ff-ff-ff-ff-ff-ff' -and
([ipaddress]$_.IPAddress).GetAddressBytes()[0] -notin 224..239
} | Format-Table -AutoSize
Run against the eleven-row cache from the reading section, that parser returns 11 objects and the filter cuts them to the 5 real neighbours, dropping 6 broadcast and multicast rows. It matches no column header, no Interface: line and no IPv6 row, which is what keeps the object count honest.
Where this matters
- Hardware was replaced: one host out of forty cannot reach the rebuilt server, and it is holding the retired NIC’s MAC until the entry ages out or you delete it.
- An address conflict alert appeared overnight: the event gives you one MAC, a third machine’s cache gives you the other, and the pair tells you which two devices to go and look at.
- A gateway failover went half-noticed: after an HSRP or VRRP switch the virtual MAC moved, and any host still caching the old one keeps sending frames to a router that is no longer forwarding.
- An unmanaged device needs identifying: no DNS record, no agent, no inventory entry, but the OUI in its MAC names the manufacturer and narrows it to a class of hardware.
- A multi-homed server sends traffic out of the wrong adapter: the routing table decides the interface and the ARP cache proves which one actually resolved the next hop.
- A test rig needs a fixed mapping: pinning an address to a MAC lets you exercise a failure path without touching DHCP, as long as the entry is removed afterwards.
Tips and limitations
- ARP is a local-subnet protocol. The cache can only ever hold on-link neighbours, so for anything off-subnet the MAC you see is the router’s, not the destination’s. If you are not sure an address is on-link, the IP subnet calculator settles it.
-aand-vread the cache and need no rights.-sand-dwrite to it and need an elevated prompt.- Entries age out, and Microsoft does not document the timer. Treat any absence as “not resolved recently” and force a resolution before drawing a conclusion.
arpis IPv4 only. IPv6 uses Neighbor Discovery, and its cache is readable only throughGet-NetNeighbor -AddressFamily IPv6ornetsh interface ipv6 show neighbors.arp -ashows the active store. An entry created in the persistent store shows up there only once the stack has loaded it, so check the store you wrote to before deciding a write failed.- A wrong static entry fails silently. There is no error and no log entry, only traffic to one address that stops arriving, which is why every static entry in this article is paired with its removal.
- The cache proves what a host believes, not what is true. Where the answer has to be authoritative, the switch’s own MAC address table is the better source.
Official documentation
- arp: Windows Commands | Microsoft Learn
- Netsh commands for Interface IPv4: add, set, show and delete neighbors | Microsoft Learn
- Get-NetNeighbor | Microsoft Learn
- New-NetNeighbor | Microsoft Learn
- Event ID 4199 and duplicate address detection | Microsoft Learn
Related tools
- Network diagnostics tool: runs the reachability checks you would otherwise type by hand before reading the cache.
- IP subnet calculator: confirms whether an address is on-link, which decides whether ARP can answer for it at all.
Related guides
- route print and route add on Windows: the layer 3 half of the same question, and the same three-tools-three-defaults trap for persistence.
- tracert and pathping in Windows: when hop 1 is not the gateway you expected, the ARP cache says which device answered for it.
- ipconfig command in Windows: beyond /all: which adapter owns which subnet, and the MAC of the local interface itself.
- Test-NetConnection in Windows: the modern way to force the resolution that populates the cache.
- NETSTAT command in Windows: once you know which device an address belongs to, this shows what it is talking to.
- Windows Command Line (CMD) Cheat Sheet: the one-line forms of these commands alongside the rest of the networking set.