Private Route 53 zones replace the answer for a name instead of merging with the public record, and that surprises people at the worst moment. Add one internal record for api.example.net in a private zone for example.net, associate it with a VPC, and every other name under that parent now answers from the private zone alone. A client that could reach www.example.net yesterday gets a negative answer today. The walkthrough below starts from that failed query and works outward to the zone that answered it.

Start from the query, not from the console

Reproduce the failure on the client that reported it, and record the exact name and record type. A name that fails for A may still resolve for AAAA, and a browser retrying a cached name hides which one broke. Run the query twice: once through the resolver the client actually uses, and once with tracing that bypasses it.

dig +short api.example.net
dig www.example.net +noall +comments +authority
resolvectl query www.example.net

The second command is the important one. Its status line and authority section name the zone that produced the answer, so you can tell a negative answer from the private zone apart from a timeout, a blocked port or a resolver that never received the query. resolvectl shows which upstream the host asked, which is often not the resolver you assumed the VPN pushed.

If the traced query returns the public address while the normal query does not, the public record is intact and the private view is answering. That single comparison decides which half of the rest of this page applies.

Read which zone answered

A response carrying NXDOMAIN with the private zone's SOA in the authority section is a definitive answer from that zone, not a lookup failure. Route 53 Resolver selected the private hosted zone because its name matched, then found no record of the requested type, and the documented behaviour is to return the negative answer instead of forwarding the question to public DNS.

Selection follows specificity. With several overlapping private namespaces, the most specific matching zone wins, and a broader private parent does not get a second chance at a name that the selected child zone lacks. Forwarding rules add another layer on top, since a domain-specific Resolver rule can win over the private hosted zone association. Enumerate the associations and rules before you conclude that a zone is empty.

aws route53 list-hosted-zones-by-name --dns-name example.net.
aws route53 list-resource-record-sets --hosted-zone-id Z0EXAMPLEID --query "ResourceRecordSets[?Type=='A'].[Name,ResourceRecords[0].Value]"

Both commands need credentials for the account that owns the zone, and the second needs the zone id from the first. Substitute your own id; the placeholder above is deliberately not a real one.

Why the private zone wins

The association is what makes the zone visible to a VPC, and the Resolver in that VPC is what applies it. A private zone that is not associated with the VPC the client's traffic arrives from has no effect at all, while an association with the wrong VPC produces a private answer for the wrong population. Check the association list before editing records, because a zone can be correct and still invisible.

Overlapping namespaces are the second source of confusion. A private zone for example.net and a private zone for api.example.net both match an API query, and the more specific one answers. That is useful when you want a narrow internal namespace, and dangerous when the narrow zone is missing a record that the broader one has. The private hosted zone considerations in the Route 53 documentation describe both the fall-through behaviour and the rule precedence, and they are worth reading before you add a second overlapping zone.

None of this changes the public zone. Public resolvers still answer the public records correctly, which is why the failure looks like a client problem to everyone outside the VPC.

Choose the namespace before you add records

If only a new family of internal services needs private names, give it a dedicated private subdomain and leave the public parent alone. A name like internal.example.net cannot shadow www.example.net, and the blast radius of a missing record is limited to names you invented for the purpose. The cost is that existing URLs and certificates may not match the new name.

When public and private users must keep the identical hostname, split-view DNS is the supported design and it requires you to maintain the records in each view explicitly. A broad private zone with one record in it does not preserve the public dependencies beneath that zone; it replaces them with a zone that has one record.

Inventory the names an application actually calls before you point a client population at the private view. Login hosts, static asset hosts, redirect targets and API endpoints are the ones that break first, and they are usually spread across several teams' records.

Trace the resolver path, then the route

A laptop only sees the private view if its queries reach the resolver that has the association. Editing a hosts file on the VPN server does not publish anything to connected clients, and a client configured with a public resolver will keep getting public answers while the tunnel is up. Confirm the resolver address the client received, then repeat the query from the client rather than from the server.

Once the name resolves, test the path to the returned address as a separate step, for example with ip route get against the address the query returned. A correct private answer with no route to the address is a routing problem, and a missing-record answer is a DNS-view problem; treating them as one issue produces fixes that do not stick. Keep the service's TLS hostname and certificate checks unchanged, because a private address changes where the connection goes and nothing about which certificate is valid.

Finish by recording the zone, the association, the resolver and the names you verified in the change note. The next person to add a record will then know which namespace is authoritative for that client population.

Sɛbɛn minnu bɛ tali kɛ