Skip to content

Known limitations

These are deliberate trade-offs, with the reasoning and the practical mitigation for each. None of them block the common use cases; they are here so nothing surprises you.

DTM steers traffic with DNS, so a client that has cached an answer keeps using it until the TTL expires. Failover time is therefore detection time plus the record TTL. This is inherent to every DNS-based traffic manager (including Azure Traffic Manager).

Mitigation: because DTM serves private zones where you control the resolvers, you can set low record TTLs safely. Combined with a short health-check interval, this gives failover in the low tens of seconds. See load-balancing methods.

The weighted method shapes traffic by returning one value per query in proportion to your weights. Downstream resolvers cache that single answer for the record TTL and clients reuse connections, so the realized split at the application tier is approximate: a 5% canary can receive materially more or less than 5% of real traffic. This is inherent to DNS-based traffic shaping (Azure Traffic Manager’s weighted routing carries the identical caveat).

Mitigation: set a low TTL (5 to 30 seconds) on the weighted record and treat the split as approximate. Selection itself is made per query; the smearing is entirely downstream.

DTM does not sign zones with DNSSEC.

Why it is acceptable: DTM serves DNS inside a private Azure VNet to network-segmented clients, where the off-path spoofing DNSSEC defends against is outside the threat model. Mitigation: rely on the private-network boundary (NSGs, VNet isolation) and TLS on the management surfaces.

Region-aware routing needs subnet mappings

Section titled “Region-aware routing needs subnet mappings”

The region and geo-failover load-balancing methods identify a client’s region from its source address. Without subnet mappings, DTM assumes every client is in the serving node’s own region, so it cannot distinguish clients that are actually elsewhere and steer them to a nearer region.

Mitigation: create subnet mappings (or use Azure region discovery) before relying on region-aware routing.

Region-aware routing sees the forwarder, not the client

Section titled “Region-aware routing sees the forwarder, not the client”

DTM identifies the client by the source address of the DNS query, for region filtering and for client affinity. It does not use EDNS Client Subnet (RFC 7871). When queries reach DTM through a forwarding resolver, every client looks like that resolver: region filtering resolves every client to the forwarder’s region (or, if the forwarder’s address is in no subnet mapping, to the serving DTM node’s own region), and client affinity pins all clients behind one forwarder address to the same backend. Nothing errors; the routing quietly collapses to one bucket. Common topologies where this applies:

  • Azure Firewall DNS proxy (hub-and-spoke).
  • Azure DNS Private Resolver outbound endpoints.
  • On-premises resolvers conditionally forwarding into Azure.
  • AKS CoreDNS forwarding a zone to DTM (as in AKS: publish services with ExternalDNS), so pod queries arrive from CoreDNS.

Changing the load-balancing method does not avoid this. Region filtering runs before every method, not only region and geo-failover: when a record’s values carry region tags, a failover, weighted or round-robin record is narrowed to the forwarder’s region too. Values get those tags automatically when their addresses fall in a subnet mapping. What does work behind any forwarder is health filtering (unhealthy backends are dropped), and crossing to another region when no backend in the selected region is healthy.

Mitigation: have clients query DTM directly (point the VNet DNS or the client’s resolver at DTM rather than a forwarder). If each client region has its own forwarder, region filtering works at the granularity of the forwarders’ locations (see load balancing). Where a single shared forwarder is unavoidable, a record ignores region for every client only if none of its values carries a region tag: every value’s address is outside all subnet mappings, and no region was set on it by hand.

A cluster cannot span networks that cannot route to each other

Section titled “A cluster cannot span networks that cannot route to each other”

Replication is peer-to-peer with no leader and no proxy, so every node in a cluster must be able to reach every other node. One cluster spans exactly one routable domain: a VNet, a set of peered VNets, or VNets joined by a transit connection (hub-and-spoke through a firewall or NVA, Virtual WAN, ExpressRoute). A node that cannot reach its peers keeps serving DNS from its own copy while quietly diverging, so the failure mode is silent rather than loud.

Mitigation: decide the cluster boundary before deploying (see deployment). Disconnected estates, such as separate subsidiaries or an unintegrated acquisition, get a separate cluster each rather than one spanning cluster.

Cluster nodes replicate changes among themselves, and they are eventually consistent rather than transactionally consistent. A change to a zone, record or override reaches every reachable node within seconds. Any other change (users, TSIG keys, health checks, forwarders and settings) typically reaches every node within a minute. A node that was unreachable when a change was made catches up automatically. If two administrators edit the same object on different nodes within the same brief replication window, the cluster converges on one of the two edits.

Mitigation: this is vanishingly rare in practice (objects are operator-managed and concurrent edits to the same row are unusual). Keep cluster nodes time-synced (Azure VMs are NTP-synced by default) so ordering is well-behaved.

Readiness guards during bulk writes and very large datasets

Section titled “Readiness guards during bulk writes and very large datasets”

/readyz includes two replication-health components:

  • replication_freshness turns the node not-ready (503) when no full-state sync has been applied within the staleness window: the node is genuinely behind its peers.
  • replication_queue reports the depth of the replication broadcast queue, which holds this node’s writes not yet passed on to enough peers. At or over its threshold (default 5000) the component names the depth in its reason, but it does not turn the node not-ready. A queue that cannot be measured at all still does, because that means replication is wedged.

A very large dataset (one whose full-state sync legitimately takes minutes) could formerly hold the freshness guard red even though DNS was serving normally. Two behaviours keep the components honest:

  • The freshness window adapts to how long a full sync takes on your dataset, so a large dataset is not falsely marked stale; only a genuine stall trips it.
  • The queue threshold is tunable: set replication.readyz_queue_threshold in the server YAML, or the DTM_REPLICATION_READYZ_QUEUE_THRESHOLD environment variable (0 or unset keeps the default 5000). It sets the depth at which /readyz names the backlog; the alert’s threshold is set in the alert rule, at the same default.

Mitigation: none needed in normal operation. A 503 from the freshness component is a deliberate “drain me” signal, not data loss: the DNS data plane keeps answering, and replication reconciles automatically once the next full-state sync lands. A large bulk import can deepen the queue for a while, which /readyz reports and the node keeps serving through. See troubleshooting for how to read the /readyz component detail.

Override records: UI writes are admin-only, weights are API/Terraform-only

Section titled “Override records: UI writes are admin-only, weights are API/Terraform-only”

Override records can be created, edited, enabled/disabled, and deleted from the web UI by admin-role users, behind confirmation guardrails that restate the effect before committing; readonly and editor roles see the list read-only in the UI (editors can still write via the REST API). Two things remain API/Terraform-only: explicit per-value weighted percentages (the UI’s weighted mode splits traffic evenly) and, as everywhere, automation-owned state; the Terraform provider stays the recommended steady-state owner. ALIAS records, by contrast, are creatable from the normal record form in the UI without the extra guardrails (UI write controls are admin-only across the board; editors write via the API or Terraform).

Auto-populated records: two accepted trade-offs

Section titled “Auto-populated records: two accepted trade-offs”

A record with auto_populate set may legitimately hold zero values: the service exists, and right now nothing is registered to it. Two standards-correct consequences are worth knowing before you create one:

  • A pre-created record suppresses wildcard synthesis at its name. Per RFC 4592, a name that exists is its own closest encloser, so a *.example.internal wildcard stops covering api.example.internal the moment an auto-populated api record is created, even while it is empty. The name answers NODATA instead of the wildcard’s data. Creating an empty placeholder feels inert; it is not.
  • A secondary loaded by AXFR answers NXDOMAIN where the primary answers NODATA. A BIND zone file is one line per value and has no way to say “this name exists with no data”, so a value-less record contributes nothing to the transfer. DTM-to-DTM replication is unaffected (it ships records, not zone files), and the JSON bundle round-trips these records (health-check template secrets aside, which the export masks). The BIND export leaves a comment line so a human reading the file can see the record exists.

The supported, fully-hardened deployment is the Azure Marketplace image, which is built on Ubuntu 24.04 LTS. The DTM binary also runs on other modern Linux distributions, but only the Marketplace image ships the complete tested baseline.

Mitigation: deploy from the Marketplace image for production. See Deploy.

Access tokens from POST /api/v1/auth/login last 24 hours. Login also returns a refresh token (rotated on each use) that POST /api/v1/auth/refresh exchanges for a fresh access token; a session that goes unused for 7 days expires and needs a new login.

A login, and each refresh, reaches every node within seconds, so any node can serve the next refresh. If a node was unreachable when you signed in, it may ask you to sign in again, once.

Mitigation: the web UI refreshes its session transparently. Automation should use a service-account token instead of logging in: long-lived (up to 10 years or non-expiring), bound to one role, revocable at any time. One caveat: a service-account token is validated on the node that issued it, so point the automation that uses it at that node.

Signing in right after your sessions end can need repeating during an upgrade

Section titled “Signing in right after your sessions end can need repeating during an upgrade”

Some actions end all of a user’s sessions: changing or resetting their password, changing their role, disabling or deleting the user, and DTM detecting a reused refresh token. From v1.0.5 this reaches every node, including a node that was down or unreachable when it happened, which catches up at its next replication exchange with its peers.

The cost: in a mixed-version cluster, a user who signs in within 5 minutes of their sessions being ended (for example, straight after changing their password) is asked to sign in once more, at the first refresh of that new session. Nothing else is affected.

Mitigation: finish the upgrade, and remove any node you have retired (a node that is absent but was never removed still counts as an older node). Once every member runs v1.0.5 or later, sessions started from then on are not asked again. Automation that refreshes a session should treat a 401 from POST /api/v1/auth/refresh as a cue to sign in again.

A bulk import request (POST /api/v1/zones/import) carries at most 20 MB, 50 zones, and 50,000 records per zone, whether the payload is JSON, CSV, or BIND; a bulk request over the body or zone cap is rejected whole and writes nothing, and a JSON or CSV payload over the record cap likewise. One BIND nuance: in a multipart upload, a single zone file over the record cap lands in the response’s parse_errors while the other files still commit. The incremental per-zone import (POST /api/v1/zones/{id}/import, body cap 10 MiB) behaves differently at the record cap: it truncates at 50,000 records, imports the records that parsed, and reports the truncation in the response errors array.

Mitigation: split very large migrations into batches of up to 50 zones per request. See migrating to Hayami DTM.

Restoring through the API on a cross-cluster node

Section titled “Restoring through the API on a cross-cluster node”

On a node started with --accept-cross-cluster, a restore made through POST /api/v1/restore is not checked for whether the node could start on the snapshot. A snapshot whose secrets do not open under the node’s key is staged, and the node then fails to start. dtm-server restore --local makes that check.

Mitigation: restore on such a node with dtm-server restore --local, and add --source-key-file for a snapshot taken under another key, as backup and restore shows.

DTM’s database reuses freed space internally but does not automatically return it to the filesystem, so the file can grow over time.

Mitigation: run compaction periodically (quarterly or when dtm_storage_bbolt_free_pages exceeds 50% of total sustained for 6 hours, the shipped DTMBboltFreelistBloat rule’s hold). It is a per-node admin operation with no downtime during staging.

Single stable IP per region (load balancer in front)

Section titled “Single stable IP per region (load balancer in front)”

The recommended deployment gives clients the DTM nodes’ stable IPs directly, for a zero-hop query path. Some operators prefer a single frontend IP per region behind an Azure Standard Internal Load Balancer. That is supported, with two trade-offs: a small extra network hop on every query, and the load balancer’s TCP-only health probes seeing less than DTM’s own health view.

Mitigation: use direct-to-stable-IP unless your organisation standardises on load-balancer-fronted services. Either way the node IPs stay stable across upgrades.

In-VNet segmentation of DNS ports is operator-managed

Section titled “In-VNet segmentation of DNS ports is operator-managed”

The deployment’s NSG narrows the management ports (8443, 8080) to the adminAddressPrefix you choose and enforces it with an explicit deny rule.

The cluster port (7946) is enforced the same way from v1.0.4: the NSG allows it from the cluster’s own subnet (both regions’ subnets on a dual-region deployment, plus any extra ranges you list), then denies it from the rest of the virtual network (deny-gossip-vnet, priority 4020). Because the allow names the subnet, a node added later in the same subnet, including a rolling upgrade’s replacement, joins with no NSG edit. A cluster deployed before v1.0.4 keeps its NSG, which has no such deny, until you redeploy it or add the rule yourself.

Node-to-node 8443 follows the same sources from v1.0.5: allow-api-cluster (priority 111) admits the API from exactly the sources the cluster-port allow rules name, so narrowing adminAddressPrefix cuts off administrators elsewhere, not the cluster itself. A node added from the v1.0.5 add-node template also gets allow-api-peers (112), and allow-api-extra (136) where extra ranges are listed. The trade-off: these rules admit 8443 from the whole cluster subnet and every extra range, not only DTM nodes. In an existing subnet that holds other VMs, those VMs can reach the API even with the prefix narrowed. Most endpoints still need a token, but /healthz, /readyz, /metrics, the sign-in paths and the Terraform mirror (when enabled) answer without one. To keep the API reachable only from your management subnet, give DTM a dedicated subnet.

The rules live in each node’s NSG, so an in-place upgrade adds none, and a node added from the v1.0.5 template gets them on its own NSG only: calls to an older node stay blocked under a narrowed prefix, and that node’s autoheal cannot confirm a ready peer. On each older NSG, add one 8443 rule per TCP cluster-port allow rule, with the same sources: allow-api-cluster (111) from allow-gossip-tcp’s; on an add-node NSG, allow-api-peers (112) from allow-gossip-peers-tcp’s; and allow-api-extra (136) from allow-gossip-extra-tcp’s where extra ranges were listed. For example:

Terminal window
az network nsg rule create -g <rg> --nsg-name <nsg> -n allow-api-cluster \
--priority 111 --direction Inbound --access Allow --protocol Tcp \
--destination-port-ranges 8443 \
--source-address-prefixes <the allow-gossip-tcp sources>

Do not redeploy the original plan to add them: a redeployment replaces the NSG’s rule list, dropping every rule you added by hand, and can be refused for an existing VM’s changed custom data.

The DNS ports (53, 853, and 443 for DoH) are still allow-scoping only: the template ships no matching deny, because a shipped deny that missed one of your client subnets would silently break resolution. An in-VNet source outside those prefixes is therefore still admitted by Azure’s default VNet rule.

Mitigation: if you want in-VNet segmentation of DNS traffic, add your own deny rules at a priority number lower than 65000, so they are evaluated before Azure’s default allow-VNet rule, or use subnet-level NSGs. See network access.

Active Directory: coexistence, not a domain-controller DNS replacement

Section titled “Active Directory: coexistence, not a domain-controller DNS replacement”

DTM serves the record types and dynamic-update mechanics Active Directory depends on: SRV (authoritative and via RFC 2136 dynamic update), A/AAAA/CNAME/PTR/NS/TXT/MX, RFC 2136 dynamic DNS with optional TSIG, AXFR, reverse zones with automatic PTR, and scavenging of stale dynamic records. What it does not implement is GSS-TSIG (RFC 3645, the Kerberos-authenticated “secure dynamic update” Windows domain controllers and domain-joined clients use by default), and there is no special handling of the AD _msdcs.<forest> delegation. So DCs and domain-joined Windows clients cannot self-register into a DTM zone the way they register into AD-integrated DNS.

Why it is acceptable: GSS-TSIG would bind DTM to AD’s Kerberos auth model and machine accounts, a large surface that cuts against the lightweight, standards-only design. Coexistence delivers the traffic-management value without DTM joining the domain. Mitigation: keep the AD-integrated zones (the domain and _msdcs) authoritative on the DCs and run DTM alongside for the application and traffic-management zones, conditionally forwarding the AD zones to the DCs; non-Windows registrants use DTM’s RFC 2136 + TSIG path directly. Full recipe: Active Directory coexistence.

The cluster encryption key is not rotatable in place

Section titled “The cluster encryption key is not rotatable in place”

The cluster encryption key, generated at first boot and shared by every node, protects node-to-node cluster traffic. There is no API or command that re-keys a running cluster: rotating it means redeploying onto fresh first-boot secrets and restoring data from a backup.

Why it is acceptable: the key never leaves your Key Vault and your nodes, and the traffic it protects flows only between the DTM nodes inside your VNet on port 7946, a path you already restrict at the NSG. Protect the key like the admin password, and always set a backup passphrase. Mitigation: guard the Key Vault (least-privilege access, MFA, access logging), keep port 7946 node-to-node only, and if policy or a suspected exposure requires a new key, plan the redeploy with support. See rotating secrets for the full secret-lifecycle picture.

An upgrade swap needs one free address in the node subnet

Section titled “An upgrade swap needs one free address in the node subnet”

DTM upgrades a node by deploying a replacement and moving the node’s stable IP onto it, so the address your clients resolve never changes. Releasing that address from the retiring node needs one unallocated address in the node subnet at that moment, over and above the spare address the replacement was deployed on. A subnet sized exactly to its node count cannot complete the move.

When the Marketplace deployment creates the node subnet, it defaults to a /27, which makes this a non-issue. The deployment wizards default to an existing subnet, though, and that can be smaller: the problem can only be reached on a minimal subnet (a /29 or smaller, after Azure’s five reserved addresses), or an existing subnet with few free addresses. In the default Private Key Vault mode, the vault’s private endpoint also takes an address in the primary node subnet.

Mitigation: give node subnets headroom, as a /27 does. If a swap does run out, it stops and reports an address-allocation failure naming the cause; free one address in the subnet and retry the upgrade. Nothing is left half-moved by the check itself.

A swap that fails after the address is released has no automatic rollback

Section titled “A swap that fails after the address is released has no automatic rollback”

Between releasing the stable IP from the retiring node and assigning it to the replacement, no resource holds that address. If the upgrade fails inside that window (for example the replacement does not come up), there is no automatic return to the old node, and restarting the old node does not bring the stable IP back with it: it starts on a temporary address instead.

Mitigation: retry the upgrade rather than restarting the old node. The release and the assignment are both idempotent and converge on a retry, which is the supported recovery. Pointing the stable IP back at the retired node is manual Azure work and is only appropriate when the replacement cannot be recovered at all; contact support before going down that path.

Roll back only to a version whose configuration schema matches

Section titled “Roll back only to a version whose configuration schema matches”

A rollback re-renders the node’s configuration from your deployment’s template and hands it to the older binary. DTM parses its configuration strictly, so a setting the older version does not recognise is an error rather than a warning: that is deliberate, because it stops a mistyped security setting from being ignored. The practical effect is that a rollback which crosses a release that added a configuration setting will not bring the node back into service.

The node tells you so rather than failing quietly. It stops before starting the server and reports that the configuration is newer than the version being rolled back to, naming the setting involved.

Mitigation: roll back only to a version whose configuration schema matches the one you are running, and check the rollback section of the rolling upgrade guide before you start. If you need to go back further than that allows, plan it with support.

Zone-transfer interop is primary-side only (no secondary zones)

Section titled “Zone-transfer interop is primary-side only (no secondary zones)”

DTM behaves as a standards-complete primary for downstream DNS servers: outbound AXFR with an IP allowlist and optional per-zone TSIG, and outbound DNS NOTIFY (RFC 1996) on zone changes (axfr.notify_targets in the configuration reference), so mirrors no longer poll SOA serials. What it does not do is act as a secondary: there is no zone type that pulls and serves a read-only replica from an external primary (BIND, Windows DNS, or another vendor), inbound NOTIFY is refused, and IXFR is neither served nor consumed. Stub zones track a delegation and forward at query time; they are not a local replica and do not survive the upstream being down.

Two consequences to plan around: the “run DTM as a secondary first, validate, then cut over” migration ramp is not available (use the parallel-run migration patterns instead), and AD-integrated zones cannot be mirrored locally; coexistence stays forwarding-based, per the Active Directory boundary above. Mitigation: downstream consumers that need a replica (a BIND mirror, ExternalDNS reads) use outbound AXFR plus NOTIFY, which is exactly the standards pairing they expect from a primary. One interop note: each cluster node sends NOTIFY for the writes it accepted itself, so configure mirrors to accept NOTIFY from any cluster node address.

If a limitation is blocking you, email [email protected]. Several of these trade-offs have a clear path to change if there is demand.