Dynamic DNS: when your A record has to chase a moving IP
Residential ISPs hand you a public IP that quietly changes: sometimes weekly, sometimes after a modem reboot, sometimes just because. Dynamic DNS (DDNS) is the well-trodden hack that pins a stable hostname on top of an unstable IP, by having something inside your network notice the change and call an API to update the A record.
Why DDNS exists#
The DNS protocol has no opinion about how records get updated: it only cares what's in the zone at lookup time. But the "set it once and forget it" flow only works for static public IPs (data centers, business-class ISP plans with a static IP, BYOIP). Everyone else needs a way to keep their A record honest:
- A home server that hosts anything reachable from outside the LAN
- A site-to-site VPN endpoint
- A prosumer router or NAS device that exposes a management portal without asking the owner to track an IP
- A self-hosted Matrix/Mastodon/Plex/whatever
DDNS gives all of those a name that always works.
One naming collision to clear up: in the standards, "Dynamic DNS" means the DNS UPDATE opcode (RFC 2136), the in-protocol mechanism DHCP servers and Active Directory use to write records straight into a zone, normally authenticated with TSIG (RFC 8945). Consumer DDNS providers do not speak UPDATE to you: they put an HTTPS API in front and edit the zone on their side. This page is about that second kind.
How it actually works#
Three moving parts. All of them small.
- An IP-detection step. The client running on your network periodically figures out what public IP your traffic is leaving from. Common methods:
- HTTP call to a service that echoes the requester's IP (
https://ifconfig.io,https://api.ipify.org,https://cloudflare.com/cdn-cgi/trace) - Read it from your router's WAN interface (if the client is on the router)
- STUN / NAT-PMP probes (less common)
- HTTP call to a service that echoes the requester's IP (
- A change detector. Compare the freshly-observed IP against the IP currently in the DNS record (or against a cached "last known" value). If unchanged, do nothing. If changed, proceed.
- An update call. Hit the DNS provider's API to PUT/PATCH the A record with the new IP. With Cloudflare that's a single
PATCH /zones/<zone>/dns_records/<record>(update DNS record) against a zone-scoped API token.
A typical client runs all three on a tight loop: every 60-300 seconds is normal. Most of the calls are no-ops; the only one that matters is the one right after your ISP changes your lease.
What you're actually looking up#
When you dig a DDNS hostname, you get a normal A record: no special record type, no DNSSEC magic. The "dynamic" part is invisible to the resolver. It is a plain A (and usually a short TTL: 60 to 300 seconds, so when the IP changes, downstream caches do not carry the old answer for hours).
dig +short A example.duckdns.org84.249.67.149The IP above belongs to whoever registered that DuckDNS name and changes whenever their ISP rotates their lease. Your output will likely differ.
nslookup example.duckdns.org 1.1.1.1Server: 1.1.1.1
Address: 1.1.1.1#53
Non-authoritative answer:
Name: example.duckdns.org
Address: 84.249.67.149For the TTL, use dig with full answer output:
dig +noall +answer example.duckdns.org Aexample.duckdns.org. 60 IN A 84.249.67.149nslookup does not display TTL values in its default output. Use dig to inspect TTL.
DuckDNS hard-codes a 60-second TTL; you cannot configure it. That is short enough that downstream caches drop a stale answer within a minute of an IP change, which is the right behavior for DDNS. When you use your own domain (Cloudflare or otherwise), you control the TTL and should set it to 60-300 seconds for the same reason.
Compare that to a typical static A record (TTL 300s or longer) where the IP rarely changes. See dig-along for more TTL examples you can run yourself, and when-its-dns for what a stale TTL looks like in practice.
Where you've already used DDNS without knowing#
Any vendor that hands you a *.<their-domain> URL for remote access, without asking what your IP is, is doing DDNS for you. Consumer routers with a "remote management" cloud portal, NAS devices with a vendor-hosted access URL, IoT gateways: the FQDN always points at whatever public IP your modem currently holds because the device runs the three-step loop above against the vendor's own DNS infrastructure. You never see the IP rotate; you just bookmark the hostname.
Common providers#
| Provider | What you get | Notes |
|---|---|---|
| Cloudflare (DIY against your own zone) | Real DNS at your real domain. Free tier covers it. | See the snippet below for a working example. |
| DuckDNS | Free <name>.duckdns.org | Painless: sign in with an existing Google/GitHub/Twitter/Reddit login to get a token, then a simple HTTP API (spec). Each name can hold an A, an AAAA, and a TXT record. |
| No-IP | <name>.ddns.net and friends | Free tier requires monthly confirmation clicks. |
| Dynu, ChangeIP, FreeDNS (afraid.org) | Various free subdomains | Quality varies; mostly fine for non-production. |
| Vendor-native (most prosumer routers, NAS devices, and IoT gateways) | A subdomain on the vendor's domain, managed for you | Zero config: but tied to that vendor's lifecycle. |
DIY DDNS against Cloudflare#
The pattern uses a zone-scoped API token and zone ID, both kept as environment variables or in whatever secret store your IaC uses.
The three steps map directly to the loop described above:
# Step 1: detect current public IPv4 address
CURRENT=$(curl -s -4 https://cloudflare.com/cdn-cgi/trace | awk -F= '/^ip=/ {print $2}')
# Step 2: compare against the live DNS record
EXISTING=$(dig +short A home.example.com @1.1.1.1)
[ "$CURRENT" = "$EXISTING" ] && exit 0
# Step 3: update via Cloudflare REST API
# CF_TOKEN = zone-scoped API token; ZONE = Cloudflare zone ID
REC_ID=$(curl -s -H "Authorization: Bearer $CF_TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records?type=A&name=home.example.com" \
| jq -r '.result[0].id')
curl -s -X PATCH -H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"content\":\"$CURRENT\",\"ttl\":60}" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records/$REC_ID"The -4 flag on the cdn-cgi/trace call forces curl to connect over IPv4. On a dual-stack host, curl can resolve cloudflare.com over IPv6 and cdn-cgi/trace will echo back a v6 address. Feeding that into a PATCH against an A record gets a 400 from the API. Force IPv4 here; handle the IPv6 side separately with a paired AAAA-record pipeline if you need it.
One Cloudflare-specific trap: this comparison only works if the record is DNS-only (grey cloud). A proxied (orange cloud) record resolves to Cloudflare edge IPs, so step 2 never matches your WAN IP and the script fires a redundant PATCH on every run. If you want the record proxied anyway, compare against the content field the API returns instead of live DNS.
The zone ID appears in the API section of the domain's Overview page in the Cloudflare dashboard (find zone and account IDs). The API token needs the Zone / DNS / Edit permission scoped to that zone only; the dashboard's "Edit zone DNS" token template is exactly that (create an API token).
For a one-off this is fine as shell. For production use, wrap it in whatever IaC or secret manager you already run so the token is never in plaintext and you get idempotent logging.
Things that go wrong#
- TTL too long. If you set TTL to 3600 to be "nice to upstream resolvers," your hostname is wrong for up to an hour after every IP change. DDNS A records should live with TTL 60-300.
- NAT confusion. The IP your DDNS client sees depends on where it runs. A client behind a double-NAT (carrier-grade NAT, hotel WiFi, or a CGNAT ISP) reports the outer IP, which may not be reachable inbound anyway. DDNS keeps the name fresh; it does not fix bad NAT.
- IPv6. Most consumer ISPs hand out a delegated prefix that also changes. If you publish AAAA records for an inside host, you need an IPv6 DDNS pipeline too: same shape, separate record.
- Rate limits. Public IP-echo services (
ifconfig.io,ipify.org) will rate-limit a misbehaving client. If you poll every 10 seconds from every host on your LAN you'll see 429s. - Provider lock-in. Vendor DDNS (hostnames under the vendor's own domain) only works while you stay on that vendor. If you change devices, the hostname does not move with you. DIY against your own domain does.
- Dangling names. When you stop running the client, the record keeps pointing at your last IP, and your ISP eventually hands that IP to a stranger. Anything still trusting the hostname (VPN configs, webhooks, bookmarks) now connects to whoever inherited the address. Delete DDNS records you no longer maintain. On providers that recycle expired free hostnames (No-IP deletes unconfirmed ones after a redemption window and anyone can re-register them), the name itself can end up in someone else's hands.
Related#
- A record: what DDNS is actually writing
- AAAA record: the IPv6 counterpart, same DDNS loop applies
- TTL trade-offs: why short TTLs matter for DDNS
- When it actually is DNS: TTL gotchas, negative caching, and how DDNS interacts with them
- The dig-along: hands-on TTL and record inspection
- nslookup and dig: tool comparison and reference
- DNS fundamentals: how the resolver sees an update