A fresh DNS Server role on Windows Server 2022 resolves internet names through root hints, at a 3,497 ms median per cold lookup on our test box. One run in five took longer than 8 seconds. Two forwarders brought the same lookups down to a 12 ms median (benchmarked October 2026 on Windows Server 2022 build 20348). That gap decides most of what you configure after the role install, so this guide measures it first and then builds the rest of the server around it.
We built two Server 2022 VMs, DNS01 and DNS02, and ran every step in PowerShell, with DNS Manager screenshots where the GUI hides a setting. The walk-through covers the role install, a forward and a reverse lookup zone, records, forwarders, a conditional forwarder and aging with scavenging. Every command and output below comes from those two servers. On Windows Server 2025, follow our DNS Server guide for Server 2025 instead.
Lab layout and the measured results
The lab is two workgroup servers on one /24, both cloned from the same Windows Server 2022 Standard image with 4 vCPU and 4 GB RAM. Neither is a domain controller, so every zone here is file-backed. On a domain controller the same cmdlets work, but zones can also be stored in Active Directory, which changes replication and dynamic update options (see our Active Directory Domain Services guide).
- DNS01 (10.0.1.10): primary for
corp.example.comand the1.0.10.in-addr.arpareverse zone, forwards to 1.1.1.1 and 9.9.9.9. - DNS02 (10.0.1.11): primary for
partner.example.net, which DNS01 reaches through a conditional forwarder. It also served as the client for remote queries and as the scavenging test box. - Gateway (10.0.1.1): plain NAT to the internet, no DNS filtering in between.
The numbers below come from that pair. Each lookup ran with both the server cache (Clear-DnsServerCache) and the client cache (Clear-DnsClientCache) emptied first, five runs per name across five names.
| Measurement on DNS01 | Result |
|---|---|
DNS role install, Install-WindowsFeature (DNS02) | 193.6 s, no restart |
| Cold lookup via root hints, median (25 runs) | 3,497 ms |
| Cold lookup via root hints, slowest | 9,564 ms; 1 of 25 lookups failed |
| Cold lookup via forwarders 1.1.1.1 and 9.9.9.9, median (25 runs) | 12 ms |
| Cold lookup via forwarders, slowest | 464 ms |
| Cached answer, median (5 runs) | 2 ms |
| Conditional forwarder to DNS02, first query | 20 ms |
Root hints worked every time but one, yet a 3.5 second median is long enough for clients to notice, and a resolver that sometimes takes 9.5 seconds will trip application timeouts. The forwarder column is mostly the round trip to Cloudflare, because the forwarder answers from its own cache even when ours is empty. We would run forwarders on any Windows DNS server that has a working upstream, and keep root hints only as the fallback.
Step 1: Set a static IP and a DNS suffix
A DNS server needs an address that does not move, and it should set its own resolver to itself. We changed the address after the role install on both VMs, and each time the DNS Server log recorded event 408, “could not open socket for address”, naming the server’s own address while it briefly disappeared during the switch from DHCP to static. Do this step first and the service starts on the right address from the beginning.
Run it from the VM or hypervisor console, not over SSH or PowerShell remoting. The first command drops the leased address at once. A remote session dies before the second line runs, and the server is left with no IPv4 address. If that happens, Set-NetIPInterface -InterfaceAlias Ethernet -Dhcp Enabled from the console brings the lease back. Open PowerShell as Administrator and replace the addresses with your own:
Set-NetIPInterface -InterfaceAlias Ethernet -Dhcp Disabled
New-NetIPAddress -InterfaceAlias Ethernet -IPAddress 10.0.1.10 -PrefixLength 24 -DefaultGateway 10.0.1.1
Set-DnsClientServerAddress -InterfaceAlias Ethernet -ServerAddresses 127.0.0.1
Turning DHCP off removed the leased address on its own, so New-NetIPAddress had nothing to clash with. Check the result before you install anything, because every later step binds to this address and the origin column tells you whether the first command took effect:
Get-NetIPAddress -InterfaceAlias Ethernet -AddressFamily IPv4 | Select IPAddress,PrefixLength,PrefixOrigin
DNS01 reported a manual binding. A Dhcp origin here would mean the lease is still active and the address can change under the DNS service later:
IPAddress PrefixLength PrefixOrigin
--------- ------------ ------------
10.0.1.10 24 Manual
The second half of this step matters on workgroup servers. A server that is not joined to a domain has no primary DNS suffix, and the DNS role writes the bare host name into the NS and SOA of every zone you create. On DNS01 that produced an NS record of dns01., a single-label name that no client outside the box can resolve. DNS02 got the suffix before its zones existed and wrote dns02.partner.example.net. plus a matching A record on its own. Set the suffix and the host name, then restart:
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name 'NV Domain' -Value 'corp.example.com'
Rename-Computer -NewName DNS01 -Force
Restart-Computer
Domain members already carry the domain as their suffix, so skip the registry line on those. If you forget it on a workgroup server, the fix for zones that already exist is in Step 4.
Step 2: Install the DNS Server role on Windows Server 2022
One cmdlet installs the role, the DNS Manager console and the DnsServer PowerShell module. The Add Roles and Features wizard in Server Manager installs the same feature set if you prefer the GUI, but the cmdlet is faster to repeat on a second server:
Install-WindowsFeature -Name DNS -IncludeManagementTools
On DNS02 the install took 193.6 seconds and needed no restart, so you can add the role to a running server during working hours. The Feature Result column lists the four pieces it added, and DNS Server Tools is the one that holds both the console and the PowerShell module, which is why -IncludeManagementTools is worth keeping even on a server you plan to manage remotely:
Success Restart Needed Exit Code Feature Result
------- -------------- --------- --------------
True No Success {DNS Server, Remote Server Administration Tools, DNS Server Tools, Role Administration Tools}
Run it once and wait. On DNS01 our first call was still running in the background when a second Install-WindowsFeature started, and the second one returned Success False with Exit Code Failed after 25.8 seconds even though the role finished installing. Check the real state with Get-WindowsFeature DNS,RSAT-DNS-Server before you retry anything.
The role also opens the firewall for you, so there is no need to add rules for port 53 by hand. List the rules it created anyway, because a group policy that manages the firewall can disable local rules without telling you.
Get-NetFirewallRule -DisplayGroup 'DNS Service' | Select DisplayName,Enabled,Direction,Profile,Action | Format-Table -AutoSize
Six rules arrive with the role, all enabled on every profile, including the RPC pair that DNS Manager uses for remote administration:
DisplayName Enabled Direction Profile Action
----------- ------- --------- ------- ------
All Outgoing (TCP) True Outbound Any Allow
DNS (UDP, Incoming) True Inbound Any Allow
RPC (TCP, Incoming) True Inbound Any Allow
All Outgoing (UDP) True Outbound Any Allow
DNS (TCP, Incoming) True Inbound Any Allow
RPC Endpoint Mapper (TCP, Incoming) True Inbound Any Allow
The service listens on every address the server owns, IPv4 and IPv6 loopback included. The DnsServer module that came with it is version 2.0.0.0 and exports 134 cmdlets on this build. Zone files land in C:\Windows\System32\dns. If a firewall upstream blocks TCP 53, large answers and zone transfers fail while UDP queries still work. Open both protocols between clients and the server (our Windows Server firewall guide covers custom rules).
Step 3: Create the forward lookup zone
A forward lookup zone maps names to addresses, and it is the zone every client query lands in first. On a non-domain server it lives in a text file, and the dynamic update choice is None or NonsecureAndSecure. Secure updates exist only for Active Directory integrated zones. Create the zone with updates off for now:
Add-DnsServerPrimaryZone -Name 'corp.example.com' -ZoneFile 'corp.example.com.dns' -DynamicUpdate None -PassThru
The zone comes back as a primary with IsDsIntegrated False. That column decides which dynamic update modes the zone accepts, and the next error shows what happens when you ask for the wrong one.
ZoneName ZoneType IsAutoCreated IsDsIntegrated IsReverseLookupZone IsSigned
-------- -------- ------------- -------------- ------------------- --------
corp.example.com Primary False False False False
Our first attempt asked for -DynamicUpdate Secure on this file-backed zone, and the cmdlet failed with WIN32 9611. The failure is only half a failure, though: the zone was created anyway with updates set to None, so re-running the corrected command then failed with WIN32 9609 because the zone already existed. This is the error pair to expect:
Add-DnsServerPrimaryZone : Failed to set property AllowUpdate for zone corp.example.com on server DNS01.
+ CategoryInfo : InvalidArgument: (AllowUpdate:root/Microsoft/...rverPrimaryZone) [Add-DnsServerPrimaryZo
ne], CimException
+ FullyQualifiedErrorId : WIN32 9611,Add-DnsServerPrimaryZone
Add-DnsServerPrimaryZone : Failed to create zone corp.example.com on server DNS01.
+ CategoryInfo : ResourceExists: (corp.example.com:root/Microsoft/...rverPrimaryZone) [Add-DnsServerPrima
ryZone], CimException
+ FullyQualifiedErrorId : WIN32 9609,Add-DnsServerPrimaryZone
If you hit 9611, check Get-DnsServerZone before you retry, because the zone is probably already there. You can keep the half-made zone and change its update setting with Set-DnsServerPrimaryZone, or remove it and start clean so the zone file matches what you meant to create. Run the next block only if you got the 9611 error and want a clean start. It deletes the zone, so run the Add-DnsServerPrimaryZone command above again afterwards:
Remove-DnsServerZone corp.example.com -Force
The DNS Manager wizard (New Zone under Forward Lookup Zones) produces the same file and asks the same questions. Our forward lookup zone walkthrough goes through every wizard page if you would rather click through it.
Step 4: Create the reverse lookup zone and fix the NS record
Reverse zones answer “which name owns 10.0.1.20”, and tools from nslookup to mail servers ask that question. The cmdlet takes the network in CIDR form and names the zone for you; for 10.0.1.0/24 the zone is 1.0.10.in-addr.arpa:
Add-DnsServerPrimaryZone -NetworkId '10.0.1.0/24' -ZoneFile '1.0.10.in-addr.arpa.dns' -DynamicUpdate None -PassThru
The new zone shows IsReverseLookupZone True, and the server derived the zone name from the network ID, so you never type the reversed octets yourself.
ZoneName ZoneType IsAutoCreated IsDsIntegrated IsReverseLookupZone IsSigned
-------- -------- ------------- -------------- ------------------- --------
1.0.10.in-addr.arpa Primary False False True False
Networks that do not end on an octet boundary (a /22, for example) need one reverse zone per /24, or a classless delegation, which our reverse lookup zone guide covers along with the GUI wizard.
Single-label NS record on a workgroup server
DNS01 was built without the suffix from Step 1, so both of its zones were created with dns01. as the NS target and as the SOA primary server. A Resolve-DnsName corp.example.com -Type NS returned just dns01, which is a name no other machine can look up. The zones still answer queries, but a secondary server or a delegation will not find the primary. Run the fix below only if Resolve-DnsName corp.example.com -Type NS -Server 127.0.0.1 returns a bare dns01. With the suffix from Step 1 in place, the zones already carry the full name and the loop would only throw errors. It replaces the NS record, rewrites the SOA in both zones and adds the A record the new NS name needs, since a server without the suffix never creates one:
foreach($z in 'corp.example.com','1.0.10.in-addr.arpa'){
Add-DnsServerResourceRecord -ZoneName $z -NS -Name '@' -NameServer 'dns01.corp.example.com'
Remove-DnsServerResourceRecord -ZoneName $z -RRType NS -Name '@' -RecordData 'dns01.' -Force
$old = Get-DnsServerResourceRecord -ZoneName $z -RRType SOA
$new = $old.Clone()
$new.RecordData.PrimaryServer = 'dns01.corp.example.com.'
$new.RecordData.ResponsiblePerson = 'hostmaster.corp.example.com.'
Set-DnsServerResourceRecord -ZoneName $z -OldInputObject $old -NewInputObject $new
}
Add-DnsServerResourceRecordA -ZoneName 'corp.example.com' -Name 'dns01' -IPv4Address '10.0.1.10'
After the loop both zones carry a fully qualified name server, and an NS query now returns dns01.corp.example.com with its address in the Additional section. We captured the forward zone after the fix, which on DNS01 ran after the Step 5 records existed. A zone fixed earlier shows a lower serial:
HostName RecordType Type Timestamp TimeToLive RecordData
-------- ---------- ---- --------- ---------- ----------
@ NS 2 0 01:00:00 dns01.corp.example.com.
@ SOA 6 0 01:00:00 [9][dns01.corp.example.com.][hostmaster.corp.example.com.]
The bracketed 9 is the SOA serial. Every record change bumps it, which is how a secondary server knows there is something new to pull (the secondary DNS server guide uses it for zone transfers).
Step 5: Add A, PTR, CNAME and MX records
With both zones in place, one A record with -CreatePtr writes the matching PTR into the reverse zone, so you never maintain the two by hand. We added two hosts, an alias and a mail exchanger. The A record for the server itself is already there. With the suffix from Step 1, creating the zone adds it automatically, as DNS02 did with dns02. Only its PTR is missing, so the last line adds it:
Add-DnsServerResourceRecordA -ZoneName 'corp.example.com' -Name 'web' -IPv4Address '10.0.1.20' -CreatePtr
Add-DnsServerResourceRecordA -ZoneName 'corp.example.com' -Name 'mail' -IPv4Address '10.0.1.25' -CreatePtr
Add-DnsServerResourceRecordCName -ZoneName 'corp.example.com' -Name 'www' -HostNameAlias 'web.corp.example.com'
Add-DnsServerResourceRecordMX -ZoneName 'corp.example.com' -Name '.' -MailExchange 'mail.corp.example.com' -Preference 10
Add-DnsServerResourceRecordPtr -ZoneName '1.0.10.in-addr.arpa' -Name '10' -PtrDomainName 'dns01.corp.example.com'
List the zone to confirm what the server stored, since the Add cmdlets print nothing unless you pass -PassThru.
Get-DnsServerResourceRecord -ZoneName 'corp.example.com' | Format-Table -AutoSize
All five records are there with a one-hour TTL and a timestamp of 0, which marks them as static. That zero matters later: scavenging never deletes a static record. The dns01. NS line is the single-label problem from Step 4, captured before the fix:
HostName RecordType Type Timestamp TimeToLive RecordData
-------- ---------- ---- --------- ---------- ----------
@ NS 2 0 01:00:00 dns01.
@ SOA 6 0 01:00:00 [6][dns01.][hostmaster.]
@ MX 15 0 01:00:00 [10][mail.corp.example.com.]
dns01 A 1 0 01:00:00 10.0.1.10
mail A 1 0 01:00:00 10.0.1.25
web A 1 0 01:00:00 10.0.1.20
www CNAME 5 0 01:00:00 web.corp.example.com.
On your server the dns01 A record comes from the zone creation, or from the Step 4 fix if you skipped the suffix. Our DNS01 had neither at this point, so in the lab we added it here with -CreatePtr instead of the PTR line, which is why the listing shows it with a static timestamp. The reverse zone ends up with PTR records for .10, .20 and .25 either way. DNS Manager (dnsmgmt.msc) shows the same zone after the NS fix, with the Timestamp column reading “static” for every row:

Records added this way are static, so they stay until you delete them, which is what you want for servers and printers. Desktops and laptops should register themselves through dynamic updates instead, because that is the only kind of record scavenging can clean up. For SRV, TXT and other record types, and for adding records through the DNS Manager dialogs, our A and PTR record guide has the screen-by-screen version.
Step 6: Test name resolution
Test against the loopback address of the server first, because that takes the resolver settings of any client out of the picture. Resolve-DnsName follows the CNAME and returns the final A record in one call:
Resolve-DnsName www.corp.example.com -Server 127.0.0.1
Resolve-DnsName corp.example.com -Type MX -Server 127.0.0.1
Resolve-DnsName 10.0.1.20 -Server 127.0.0.1
The MX query also returns the A record for the mail host in the Additional section, and the PTR lookup confirms that -CreatePtr did its job:
Name Type TTL Section NameExchange Preference
---- ---- --- ------- ------------ ----------
corp.example.com MX 3600 Answer mail.corp.example.com 10
Name : mail.corp.example.com
QueryType : A
TTL : 3600
Section : Additional
IP4Address : 10.0.1.25
Name Type TTL Section NameHost
---- ---- --- ------- --------
20.1.0.10.in-addr.arpa PTR 3600 Answer web.corp.example.com
Then query it from another machine, which proves the firewall rules and the listener on the real address. A server that answers on 127.0.0.1 but not on its LAN address usually has its Interfaces tab limited to other addresses, so this second test catches what the first one cannot. From DNS02:
Resolve-DnsName web.corp.example.com -Server 10.0.1.10
Test-NetConnection 10.0.1.10 -Port 53
Both came back clean, the A record over UDP and the TCP check on port 53. Test both, because a firewall that passes UDP 53 but drops TCP 53 lets small queries through and breaks large answers and zone transfers.
Name Type TTL Section IPAddress
---- ---- --- ------- ---------
web.corp.example.com A 3600 Answer 10.0.1.20
ComputerName : 10.0.1.10
RemotePort : 53
TcpTestSucceeded : True
Clients that should use this server need it as their DNS server, either through Set-DnsClientServerAddress on each machine or, more practically, as the DNS option your Windows DHCP server hands out. The full state of DNS01 at the end of the lab, with the role, every zone and a lookup through each path, fits on one screen:

The TrustAnchors zone at the bottom was not in our first zone listing and showed up after the DNS Manager session in which we browsed the console, Trust Points included. It holds DNSSEC trust anchors, is empty here, and needs nothing from you.
Step 7: Configure DNS forwarders
Forwarders send every name the server is not authoritative for to an upstream resolver instead of walking the root hints. On DNS01 that cut the median cold lookup from 3,497 ms to 12 ms. Read the current settings before you change them:
Get-DnsServerForwarder
Before you add any, the server reports an empty list, root hints on and a 3-second timeout:
UseRootHint : True
Timeout(s) : 3
EnableReordering : True
IPAddress :
ReorderedIPAddress :
Set-DnsServerForwarder replaces the whole list, while Add-DnsServerForwarder appends to it. Use the first in scripts, because running an append twice leaves you guessing which entries were there before. The list is then exactly what you typed:
Set-DnsServerForwarder -IPAddress 1.1.1.1,9.9.9.9 -PassThru
Root hints stay enabled as the fallback, which is the setting we would keep: if both forwarders go dark, the server falls back to the slow path instead of failing every external lookup:
UseRootHint : True
Timeout(s) : 3
EnableReordering : True
IPAddress : {1.1.1.1, 9.9.9.9}
ReorderedIPAddress : {1.1.1.1, 9.9.9.9}
Check that both upstreams actually answer from this server before you rely on them. Test-DnsServer with the Forwarder context sends a real query to each:
Test-DnsServer -IPAddress 1.1.1.1,9.9.9.9 -Context Forwarder
Both returned Success over UDP. The test query leaves from DNS01 itself, so this also proves that outbound UDP 53 gets through the edge firewall.
IPAddress Result RoundTripTime TcpTried UdpTried
--------- ------ ------------- -------- --------
1.1.1.1 Success 00:00:00 False True
9.9.9.9 Success 00:00:00 False True
The per-name numbers show where root hints hurt, because a median hides how uneven they are. Each cell is the median of five cold lookups from DNS01, so a single slow run moves the range but not the median:
| Name | Root hints, median | Root hints, range | Forwarders, median | Forwarders, range |
|---|---|---|---|---|
| www.debian.org | 5,205 ms | 1,063 to 8,508 ms (1 failed) | 18 ms | 12 to 246 ms |
| www.kernel.org | 4,929 ms | 1,012 to 9,564 ms | 12 ms | 12 to 227 ms |
| github.com | 320 ms | 177 to 3,497 ms | 12 ms | 11 to 14 ms |
| learn.microsoft.com | 1,653 ms | 1,597 to 8,095 ms | 12 ms | 12 to 464 ms |
| www.wikipedia.org | 5,346 ms | 711 to 8,516 ms | 12 ms | 12 to 317 ms |
The spread on root hints is the real problem. github.com stayed under 400 ms after its first run. kernel.org, Microsoft Learn and Wikipedia each had a successful run past 8 seconds, and one Debian lookup failed after 8.5 seconds. No forwarder run went past 464 ms. Our first full benchmark pass, run without clearing the client cache, was misleading in the other direction: most lookups showed 11 to 12 ms because the Windows DNS client had cached them, so clear both caches whenever you measure.
In DNS Manager the same list sits on the Forwarders tab of the Properties dialog for DNS01, and the “Use root hints if no forwarders are available” box is the UseRootHint flag:

In corporate networks the forwarders are often the resolvers of the ISP or a filtering service instead of public resolvers. The cmdlet does not care which, but run Test-DnsServer against them before you switch, because an upstream that drops queries from your address costs the full timeout on every miss.
Step 8: Add a conditional forwarder
The target zone has to exist somewhere first. Build DNS02 with Steps 1 and 2, using 10.0.1.11, the name DNS02 and the suffix partner.example.net. On DNS02 we then created partner.example.net with one host record, running these on DNS02 itself:
Add-DnsServerPrimaryZone -Name 'partner.example.net' -ZoneFile 'partner.example.net.dns' -PassThru
Add-DnsServerResourceRecordA -ZoneName 'partner.example.net' -Name 'app' -IPv4Address '10.0.2.30'
Because DNS02 had its suffix set before this, the zone came up with dns02.partner.example.net. as its name server and an A record for DNS02, so nothing else is needed on that side. Everything from here on runs on DNS01.
A conditional forwarder sends one domain to one set of servers and leaves everything else on the normal forwarders. It is how you resolve a private zone that belongs to a partner or a lab without hosting a copy of it. Before we added one, DNS01 sent app.partner.example.net to Cloudflare like any other name, and the public resolver answered with the real example.net SOA. This is the query to compare before and after:
Resolve-DnsName app.partner.example.net -Server 127.0.0.1 -DnsOnly
Without the conditional forwarder the name does not exist, and the answer carries only the authority record:
Name Type TTL Section PrimaryServer NameAdministrator SerialNumber
---- ---- --- ------- ------------- ----------------- ------------
example.net SOA 899 Authority betty.ns.cloudflare.com dns.cloudflare.com 2418002667
On a server that is not a domain controller, leave out -ReplicationScope. That parameter belongs to Active Directory integrated zones, so the server has no directory partition to store the zone in. Passing it on DNS01 failed with WIN32 9901:
Add-DnsServerConditionalForwarderZone : Failed to create zone partner.example.net on server DNS01.
+ CategoryInfo : ObjectNotFound: (partner.example.net:root/Microsoft/...tionalForwarder) [Add-DnsServerCo
nditionalForwarderZone], CimException
+ FullyQualifiedErrorId : WIN32 9901,Add-DnsServerConditionalForwarderZone
Without it the zone is stored locally and points at DNS02. Read it back afterwards, because the creation cmdlet prints nothing by default.
Add-DnsServerConditionalForwarderZone -Name 'partner.example.net' -MasterServers 10.0.1.11
Get-DnsServerZone partner.example.net | Format-List ZoneName,ZoneType,IsDsIntegrated,MasterServers,ForwarderTimeout,UseRecursion
The zone type reads Forwarder, the master is DNS02, and the timeout is 3 seconds, the same as the server-level forwarders. IsDsIntegrated False means only DNS01 knows about this zone, so a second DNS server needs the same command run on it.
ZoneName : partner.example.net
ZoneType : Forwarder
IsDsIntegrated : False
MasterServers : 10.0.1.11
ForwarderTimeout : 3
UseRecursion : False
The same name now resolves to DNS02’s record. The first query took 20 ms and the next three 1 ms each from cache. In our first run, a query sent within a second of creating the zone returned SERVFAIL once and then worked, so give a new conditional forwarder a moment before you judge it:
Name Type TTL Section IPAddress
---- ---- --- ------- ---------
app.partner.example.net A 3600 Answer 10.0.2.30
UseRecursion False means DNS01 does not try to resolve the name itself if DNS02 fails to answer, according to the cmdlet reference. We did not take DNS02 down to test it. It is still the behaviour we want for a private zone: a partner outage should show up as a failed lookup, not as a public answer.
Step 9: Turn on aging and scavenging
Scavenging deletes dynamic records that have not been refreshed, which keeps laptops that left the network from holding stale A records for months. It is off at two levels on a fresh install, and both have to be on before anything is deleted. Check the server side first:
Get-DnsServerScavenging
The defaults on DNS01 show the task off with a zero interval, while the 7-day intervals are only the values new zones will inherit:
NoRefreshInterval : 7.00:00:00
RefreshInterval : 7.00:00:00
ScavengingInterval : 00:00:00
ScavengingState : False
LastScavengeTime :
Aging only matters for records that carry a timestamp, which means records registered through dynamic update (clients and DHCP) or added with -AgeRecord. The zone therefore needs dynamic updates turned on as well. Enable the server-side task first, then aging on the zone:
Set-DnsServerScavenging -ScavengingState $true -ScavengingInterval 7.00:00:00 -PassThru
Set-DnsServerPrimaryZone -Name corp.example.com -DynamicUpdate NonsecureAndSecure
Set-DnsServerZoneAging -Name corp.example.com -Aging $true -NoRefreshInterval 7.00:00:00 -RefreshInterval 7.00:00:00 -PassThru
The zone reports when it first becomes eligible. We enabled aging shortly after 22:00 UTC on 29 October, and the earliest scavenge landed on 5 November at 22:00, seven days out on the hour:
ZoneName : corp.example.com
AgingEnabled : True
AvailForScavengeTime : 11/5/2026 10:00:00 PM
RefreshInterval : 7.00:00:00
NoRefreshInterval : 7.00:00:00
ScavengeServers :
Nonsecure updates on a workgroup zone let any client register any name, and DNS Manager says so with a warning on the General tab of the zone. Keep this zone on an internal network only, or move it to Active Directory and switch to secure updates. The zone-level settings live behind the Aging button on that tab:

- 1Scavenge stale resource records
- 2No-refresh interval
- 3Refresh interval
- Scavenge stale resource records: the per-zone switch that
-Aging $truesets. - No-refresh interval: 7 days during which a client re-registering the same record does not update its timestamp, which saves zone writes.
- Refresh interval: 7 more days in which a refresh must arrive. A record not refreshed by the end of both intervals is stale.
The dialog states the one constraint that matters: the refresh interval must be longer than the longest time a client waits between refreshing its record. If your clients register through DHCP, compare it with the longest lease you hand out before you shorten anything. The server-side half is on the Advanced tab of the same Properties dialog, as “Enable automatic scavenging of stale records” with a 7-day period:

The scavenging period here is how often the server runs its cleanup task, not how old a record must be. Age is decided per zone by the two intervals above, so a 7-day period with 7 plus 7 day intervals removes a dead record between 14 and 21 days after its timestamp was last updated. A refresh inside the no-refresh window does not update the timestamp. Count from the last update, not from the last time the client checked in.
What a scavenging run deleted on the test zone
Waiting 14 days was not an option, so we tested on DNS02 with 1-hour intervals, stopped the Windows Time service and moved the clock forward. The zone held three static records (app, printer and the automatic dns02), laptop-new added with -AgeRecord at about 22:06 (stored as 22:00, since timestamps are kept to the hour), and laptop-live added the same way after the clock jump. We describe that setup instead of scripting it. Moving a clock is not something to repeat on a real server. The run and the checks:
Start-DnsServerScavenging -Force
Get-WinEvent -LogName 'DNS Server' -MaxEvents 2 | Format-List TimeCreated,Id,Message
Get-DnsServerResourceRecord -ZoneName partner.example.net -RRType A
One node visited out of nine was deleted, and it was the stale laptop record. The output below is trimmed to the 2501 event and the zone listing; the second event was a routine 3150 zone write:
Id : 2501
Message : The DNS server has completed a scavenging cycle:
Visited Zones = 1,
Visited Nodes = 9,
Scavenged Nodes = 1,
Scavenged Records = 1.
This cycle took 1 seconds.
HostName RecordType Type Timestamp TimeToLive RecordData
-------- ---------- ---- --------- ---------- ----------
app A 1 0 01:00:00 10.0.2.30
dns02 A 1 0 01:00:00 10.0.1.11
laptop-live A 1 10/30/2026 1:00:00 AM 01:00:00 10.0.2.53
printer A 1 0 01:00:00 10.0.2.60
Getting to that result exposed four behaviours that the cmdlet help does not spell out:
- Intervals and aging in one call: setting
-Aging $truetogether with new 1-hour intervals calculated the eligible time from the old 7-day value (5 November). Turning aging off and on again set it to the next hour. Set the intervals first if you change them. - Service restarts: restarting the DNS service moved
AvailForScavengeTimeto the restart time plus the refresh interval. A server rebooted every week for patching with a 7-day refresh interval can postpone scavenging indefinitely. - The 30-minute gap: three forced runs within two real minutes all logged event 2502, “no nodes were visited”, even with
-Forceand the clock moved hours ahead. The run that worked came 32 real minutes after the previous one, which fits the 30-minute limit that the 2502 event text lists as one of its causes. - Back-dating records: the
TimeStampproperty of a record object is read-only in PowerShell, so you cannot fake an old registration by cloning a record. Test with short intervals instead.
For production we would leave the 7-day intervals, enable scavenging on one server per zone, and only after every static record that matters has been checked for a zero timestamp. A record imported with a timestamp by mistake gets deleted on schedule, and the first anyone hears of it is a helpdesk ticket a fortnight later.
DnsServer cmdlets used in this guide
Everything above ran through the DnsServer module, so the whole server can be rebuilt from a script. These are the cmdlets the lab used and the job each one did. The full list of 134 is in the DnsServer module reference.
| Cmdlet | What it did here |
|---|---|
Add-DnsServerPrimaryZone | Created the forward zone (-Name) and reverse zone (-NetworkId) |
Add-DnsServerResourceRecordA | Added hosts; -CreatePtr wrote the PTR, -AgeRecord added a timestamp |
Add-DnsServerResourceRecordCName / MX | Added the alias and the mail exchanger |
Add-DnsServerResourceRecordPtr | Added the PTR for the server itself |
Remove-DnsServerZone / Set-DnsServerPrimaryZone | Removed the half-made zone; switched dynamic updates on for aging |
Get-DnsServerResourceRecord | Listed a zone with the timestamp of each record |
Set-DnsServerResourceRecord | Rewrote the SOA from a cloned record object |
Set-DnsServerForwarder | Replaced the forwarder list |
Test-DnsServer | Sent a test query to each forwarder |
Add-DnsServerConditionalForwarderZone | Sent partner.example.net to DNS02 |
Set-DnsServerScavenging | Turned on the server scavenging task |
Set-DnsServerZoneAging | Turned on aging for one zone |
Start-DnsServerScavenging | Forced a scavenging run |
Clear-DnsServerCache | Emptied the server cache between benchmark runs |
If you are building PowerShell skills alongside the server work, our list of PowerShell scripting books and the Windows Server administration books are where we would start.
DNS Server events worth alerting on
The DNS Server event log (Get-WinEvent -LogName 'DNS Server') recorded every problem in this lab before any query failed. Mainstream support for Windows Server 2022 ended on 14 October 2026 and extended support runs to 15 October 2031 (lifecycle page), so these servers will be around for years, and these are the IDs we would alert on:
- 408, could not open socket: logged on both servers for their own address when the IP was switched from DHCP to static after the role install. Expect it once around a readdressing; repeated 408s mean the server is bound to an address it no longer owns.
- 2501, scavenging cycle completed: carries the counts of visited and scavenged records. A sudden jump in Scavenged Records is the first sign a static record was aged by mistake.
- 2502, no nodes were visited: scavenging ran but no zone qualified, or a cycle already ran within 30 minutes. A week of only 2502 means aging is off on the zone you think is being cleaned.
- 3150, zone written to file: logged each time a file-backed zone is saved to disk with a new version number, so the rate tracks how busy dynamic updates are.
- 5504, invalid domain name in a packet: DNS01 logged 318 of these by the end of the lab, and the one we opened came from 202.12.27.33, the M root server. Only the root-hint lookups talk to root servers directly, so a server that uses forwarders should log far fewer.
A weekly Get-WinEvent -LogName 'DNS Server' | Group-Object Id gives the counts per ID in one line each, which on DNS01 was enough to spot the 5504 burst and the two 408s without opening Event Viewer.