Guide

DHCP Not Assigning Addresses: Same Subnet or Relay

A client that never gets an address tells you one fact: no usable offer came back. It does not tell you whether the discover died on the wire, at the pool, in the relay, or on the way home. Same-subnet DHCP and relayed DHCP fail for almost entirely different reasons, and running the wrong cause list is how an afternoon disappears. This guide splits the problem first, then works each half: pools, exclusions and conflicts on one side; ip helper-address, giaddr, the return route and DHCP snooping on the other. For the build itself rather than the repair, see configure an IOS DHCP server and configure DHCP relay.

Read the client, then split the problem

On Windows or macOS a failed lease leaves a 169.254.x.x address. That is APIPA: the client gave up waiting and picked a link-local address for itself. Lab Linux clients usually do not — the DHCP client exits without a lease and ip addr shows the interface with no IPv4 address — but that varies by image, so read the interface instead of assuming. An IOS interface set to ip address dhcp reads unassigned in show ip interface brief. All three say the same thing and nothing more: nothing usable came back. None of them says where the exchange stopped.

Before touching DHCP, prove the path. Put a static address from the target subnet on the client and ping its default gateway. If that fails, DHCP is innocent — you have a cable, an access port in the wrong VLAN, a trunk that does not carry the VLAN, or a down SVI, and no amount of pool tuning fixes any of them. If the ping works, the wire and the VLAN are fine and the fault really is in DHCP.

Now ask the one question that decides the rest of this page: is the DHCP server in the client's broadcast domain? A discover is a broadcast to 255.255.255.255 on UDP 67, and a router will not forward it — that boundary is what a router is for. Same VLAN means the server hears the client directly and the four-message exchange is a local conversation. A router, an SVI or a router-on-a-stick subinterface between them means a relay has to carry it, and the cause list changes completely.

QuestionServer on the client's subnetServer across a router
How the discover reaches the serverAs a broadcast, on the interface that owns the subnetAs a unicast from the relay, stamped with giaddr
How the server picks a poolBy the receiving interface's own addressBy giaddr
First commandshow ip dhcp pool on the servershow ip interface <int> | include Helper on the relay
Most common causeThe pool network statement does not match the interface subnetThe helper is on the server-facing interface
Quietest causeExclusions or conflicts have eaten the usable rangeThe server has no route back to the client subnet
When nothing comes back at allNo pool covers the interface the request arrived onThe relayed packet never arrived, or no pool matches giaddr

Same subnet: read the pool before you read the config

Start with show ip dhcp pool, not show running-config. It names each pool, the subnet it serves, and how much of that subnet is leased. Three readings matter: the pool is not listed at all, the pool's subnet is not the client's subnet, or the leased count has reached the total. show ip dhcp binding then shows what has actually been handed out — an empty binding table on a server that should be busy means no request ever completed, but read it alongside the pool rather than on its own.

An IOS router serves a local pool only when the pool's network statement covers the primary address of the interface the request arrived on. network 10.10.10.0 255.255.255.0 on a router whose LAN interface is 10.10.11.1 matches nothing, and IOS says nothing about it: the pool is accepted, it is simply never selected. Compare the pool's network line against the address and mask on the client-facing interface, which show running-config interface <int> gives you in full; show ip interface brief has no mask column, so it cannot settle this.

ip dhcp excluded-address is a global command, not a pool command, and it applies to every pool on the box. Two ranges added months apart can between them cover the whole usable space while neither looks wrong on its own. Add the exclusion ranges up and compare the total against the size of the subnet: a /24 left with a handful of usable addresses has been excluded down to that. The exclusion lines sit near the top of the config, far from the pool you are reading.

show ip dhcp conflict is the check almost nobody runs. Before offering an address the IOS server pings it, and if something answers — or if a client declines the address after finding it already in use — the address goes onto the conflict list and is never offered again until you clear it. A LAN with a few statically addressed hosts inside the pool range bleeds addresses into that list one boot at a time, and a pool that worked last month runs dry. clear ip dhcp conflict * empties it; excluding the statics is what stops it refilling.

One thing not to check: service dhcp. The DHCP service is on by default and IOS never writes that line, so its absence from the running config proves nothing whatsoever. The line that does get written is the negation. If someone turned the service off, no service dhcp is in the config — search for that instead.

! Server and clients on the same subnet — read state first
R1# show ip dhcp pool
R1# show ip dhcp binding
R1# show ip dhcp conflict

! The two things that must agree — brief has no mask column
R1# show running-config interface Ethernet0/0
R1# show running-config | section ip dhcp pool

! Statics inside the range: exclude them, then clear what they poisoned
R1(config)# ip dhcp excluded-address 10.10.10.1 10.10.10.20
R1# clear ip dhcp conflict *

! Not a check. IOS never writes "service dhcp" — look for the negation
R1# show running-config | include no service dhcp

The same-subnet order of checks: pool, bindings, conflicts, then the interface the request lands on.

Across a router: the helper belongs on the client-facing interface

ip helper-address goes on the interface the clients broadcast into — their default gateway — never on the interface that faces the server. On the server-facing routed link it is accepted, it looks correct in a config review, and it does nothing at all, because the client's broadcast never lands there. This is the most common relay fault by a wide margin, and it is the one that survives being read.

So check it rather than reading it. Run show ip interface <interface> | include Helper against the interface whose address is the clients' default gateway. IOS reports that interface's helper state either way, so read what the line says rather than take any output as a pass: a line saying the helper address is not set means the helper is sitting on some other interface, and an address means that is the server this interface will unicast to. If the address shown is not your DHCP server, you have found the fault without changing anything.

That interface also needs an IP address in the client subnet, because the relay copies that address into the giaddr field of the packet it forwards. giaddr does two jobs and both are load-bearing. First, it is how the server picks a pool. By the time a relayed request reaches the server it is an ordinary unicast from the relay, arriving on the server's own local interface with the client's ciaddr still 0.0.0.0, so giaddr is the only field the server keys on to name the client's subnet. The server looks for a pool whose network covers giaddr, and if no pool does, it answers nothing at all — no offer and no NAK, and that silence is the only symptom. Second, giaddr is the return address: the server unicasts the offer straight back to it, and the relay delivers it onto the client subnet.

Two details follow from that. If the client subnet is configured as a secondary address on the relay interface, giaddr is still the primary address, so the server matches the wrong pool or none. And the pool on the server must be for the client subnet, not the server's own — a server sitting on 10.20.20.0/24 that serves 10.10.10.0/24 through a relay needs a 10.10.10.0/24 pool whose default-router is the relay's client-side address, the same address that arrived as giaddr.

show ip dhcp server statistics on the server settles whether the relayed packet ever arrived, because it counts messages by type in each direction. Discovers received still at zero means nothing is reaching the server: the helper is missing or on the wrong interface, the helper address is wrong, or something on the path drops UDP 67. Discovers received climbing while offers sent stays at zero means the packet arrives fine and no pool matches giaddr. Clear the counters before the test so the numbers belong to the attempt you are watching.

! R1 here is the relay: a router, with Ethernet0/0 as the clients' gateway.
! On a layer 3 switch that interface is an SVI instead — the rule is the same.

! WRONG — on the server-facing link this relays nothing
R1(config)# interface Ethernet0/1
R1(config-if)# ip address 10.20.20.1 255.255.255.0
R1(config-if)# ip helper-address 10.20.20.10

! RIGHT — on the interface that is the clients' default gateway
R1(config)# interface Ethernet0/0
R1(config-if)# ip address 10.10.10.1 255.255.255.0
R1(config-if)# ip helper-address 10.20.20.10

! Prove where it landed
R1# show ip interface Ethernet0/0 | include Helper

! Server: the pool is for the CLIENT subnet; default-router is the giaddr address
R2(config)# ip dhcp pool BRANCH-LAN
R2(dhcp-config)# network 10.10.10.0 255.255.255.0
R2(dhcp-config)# default-router 10.10.10.1

! Server: did the relayed request arrive, and did anything go back?
R2# clear ip dhcp server statistics
R2# show ip dhcp server statistics

Where the helper sits decides everything; the server's counters say whether the relayed request arrived.

The return path, and why it is the most-missed cause

The helper is right, the pool matches, the server's counters show a request in and an offer out, and the client still has nothing. The offer is being built and then dropped on the way home.

The server does not reply to the client. It replies to giaddr, which is an address on a subnet the server is not attached to. If the server has no route to that subnet, its own routing table discards the offer after DHCP has already handed it off, so nothing on the DHCP side of the server looks wrong — the packet is lost one layer below where you are looking. Every device you would think to check looks correct, which is exactly why this cause costs the most time.

The test is one command, run on the server rather than the relay: ping the relay's client-facing address, the giaddr. If that fails, DHCP was never going to work. Follow it with show ip route for the client subnet on the server and confirm something covers it.

Fix it the way the rest of the network is routed. A static route on the server for the client subnet, pointed at the next hop toward the relay, is fine for a two-router branch and becomes a maintenance problem past that; advertising the client subnet into the IGP from the relay scales, but only if the link between the relay and the server is in the IGP as well, because that adjacency is what carries the LAN prefix. Check the other direction while you are there — the relay needs a route to the server address it is unicasting to — though as the clients' gateway it usually has one already.

An ACL on the path is the same failure in different clothes. The relayed request travels as a unicast to UDP 67, and the reply comes back to UDP 67 as well, because the relay and not the client is the destination. A list written around what a finished client needs will happily block the exchange that would have given it an address.

! Run these on the SERVER, not the relay
R2# ping 10.10.10.1
R2# show ip route 10.10.10.0

! Static route home to the client subnet
R2(config)# ip route 10.10.10.0 255.255.255.0 10.20.20.1

! Or advertise it instead — both networks go in, because the link to R2 is
! what forms the adjacency that carries the client subnet
R1(config)# router ospf 1
R1(config-router)# network 10.10.10.0 0.0.0.255 area 0
R1(config-router)# network 10.20.20.0 0.0.0.255 area 0
! R2 needs that same link in OSPF, or it has no neighbor to learn from

The return-path test is run from the server: if it cannot reach giaddr, no offer can get home.

DHCP snooping eating the relayed packets at the edge

The fifth cause is usually self-inflicted, because DHCP snooping breaks DHCP the moment it is enabled with its defaults.

ip dhcp snooping plus ip dhcp snooping vlan 10 makes every port in that VLAN untrusted. An untrusted port may carry client messages and may not carry server messages, so the switch drops offers, acks and NAKs arriving on it. The uplink toward the relay or the server carries precisely those, so until you trust it the replies die one hop short of the client. Trust the uplink and only the uplink — ip dhcp snooping trust on the port facing the relay or server, and nothing on the access ports, which is the whole point of the feature.

The second half is option 82. An IOS switch inserts it by default once snooping is on, and it inserts it while leaving giaddr at zero, because a switch is not a relay. An IOS relay or IOS DHCP server drops a packet carrying option 82 with a zero giaddr — from its point of view that is a forged relay. Pick one end and fix it there: no ip dhcp snooping information option stops the switch inserting it, or ip dhcp relay information trusted on the receiving interface (or ip dhcp relay information trust-all globally) tells the router to accept it. The first is the usual choice in a lab; the second is what you use when the option 82 data is genuinely wanted. DHCP snooping and DAI covers the trust boundary in full.

Rate limiting is the third way snooping bites. A port carrying ip dhcp snooping limit rate set low will err-disable under a burst — a classroom of clients booting at once, or one client retrying hard — and the port goes down rather than the lease quietly failing. Check for err-disabled ports before you blame the pool.

SW(config)# ip dhcp snooping
SW(config)# ip dhcp snooping vlan 10

! Trust ONLY the port toward the relay or server
SW(config)# interface GigabitEthernet0/1
SW(config-if)# ip dhcp snooping trust

! Option 82 with a zero giaddr is dropped — fix one end, not both
SW(config)# no ip dhcp snooping information option
! ...or, on the relay
R1(config)# interface Ethernet0/0
R1(config-if)# ip dhcp relay information trusted

! What the rate limiter shut down
SW# show ip dhcp snooping
SW# show interfaces status err-disabled

Snooping defaults drop server replies on every untrusted port, the uplink included.

The order to work it in

Worked in this order, nothing is checked twice and each step rules out a whole branch of the tree.

  1. Give the client a static address in its own subnet and ping the gateway. A failure here is VLAN, trunk or cabling, not DHCP.
  2. Decide the case. Is the server in the client's broadcast domain? The two halves share almost no causes, so commit before you start typing.
  3. Same subnet: show ip dhcp pool, then the pool's network line against the client-facing interface's address and mask, then show ip dhcp conflict, then the global ip dhcp excluded-address lines.
  4. Across a router: show ip interface <client-facing> | include Helper on the relay, and confirm that same interface holds an address in the client subnet.
  5. On the server, clear and read show ip dhcp server statistics. Nothing received means the request is not arriving. Received but nothing sent means no pool matches giaddr.
  6. Ping giaddr from the server. No reply means no offer can get home, whatever the counters say — add the route.
  7. If a switch sits between the client and the relay, check snooping trust and option 82 there before anything else on that switch.

Make the client ask again

DHCP retries on its own schedule, so a fix can look like it failed simply because nobody asked again. Force it: bounce ip address dhcp on an IOS client, re-run whatever DHCP client the Linux node ships (udhcpc on the busybox-based images), or renew on Windows. A client that already holds a lease will not re-request just because you repaired the server, and a client stuck in a long backoff can sit quiet for a minute after everything is correct.

Verify the repair on three devices

One working client is not proof. Check the client: an address inside the right subnet, with the right default gateway, and a ping to something beyond the gateway. Check the server: a binding for that exact address, and a pool whose leased count moved. Check the relay: the helper line still on the client-facing interface, and a route to the server.

Then release the lease and take it again from cold, so you know the path works when you are not standing over it. If the address only ever appears after a hand-run renew, something between the client and the server is still dropping the first broadcast, and you have found a second fault rather than fixed the first. The commands for each of these live on the DHCP cheat sheet if you want them in one place.

! Client (IOS acting as one)
PC# show ip interface brief
PC# show dhcp lease

! Server
R2# show ip dhcp binding
R2# show ip dhcp pool

! Relay
R1# show ip interface Ethernet0/0 | include Helper
R1# show ip route 10.20.20.0

What a finished repair looks like from each of the three devices involved.

Frequently asked questions

Why does my client get a 169.254 address when DHCP fails?

That is APIPA, the IPv4 link-local self-assignment that Windows and macOS both fall back to. When no offer arrives, the client picks a link-local address for itself so it can still talk on the local segment. It tells you that no usable offer came back and nothing more. A Linux client in a lab normally shows no IPv4 address at all, and an IOS interface set to take its address by DHCP reads as unassigned in the interface list. All three readings mean the same thing.

Does ip helper-address go on the interface facing the server or the clients?

The clients. The relay has to catch the client's broadcast, and that broadcast only lands on the interface that is the clients' default gateway. Configured on the server-facing link the command is accepted and never fires, which is exactly why this fault survives a config review.

The server shows the request arriving but the client never gets an address. What is wrong?

One of two things. Either no pool matches the giaddr field, so the server builds no offer at all, or the server has no route back to the client subnet and the offer it did build is dropped by its own routing table on the way out. Ping the relay's client-facing address from the server. If that fails, fix the route before you look at anything else.

Should I check that the DHCP service is enabled on the router?

No. The DHCP service is on by default and IOS does not write that line into the configuration, so not finding it proves nothing. Only the negated form is written, so if someone has turned the service off you will find the no form of the command in the running config. Search for that instead.

Why did DHCP stop working the day we enabled DHCP snooping?

Snooping makes every port in the VLAN untrusted by default, and an untrusted port is not allowed to carry server messages, so offers and acks arriving on the uplink are dropped. Trust the uplink toward the relay or server. Snooping also inserts option 82 by default while leaving the giaddr field at zero, which an IOS relay or server drops as a forged relay, so either stop the switch inserting it or tell the receiving device to accept it.

Fix a dead DHCP request on a graded lab

Trace a request that goes nowhere across a real relay in CML, then upload the config for a grade.

All DHCP labs →

Practice on real Cisco IOS

Build it on real Cisco IOS and get instant pass/fail grading on your own config.